diff --git a/.github/workflows/pl-api.yaml b/.github/workflows/pl-api.yaml index be425ee81..ea503280f 100644 --- a/.github/workflows/pl-api.yaml +++ b/.github/workflows/pl-api.yaml @@ -34,7 +34,7 @@ jobs: working-directory: ./packages/pl-api run: yarn lint - - name: build + - name: Build env: NODE_ENV: production working-directory: ./packages/pl-api diff --git a/.github/workflows/pl-fe.yaml b/.github/workflows/pl-fe.yaml index ff67a2dbb..fad6f0d7b 100644 --- a/.github/workflows/pl-fe.yaml +++ b/.github/workflows/pl-fe.yaml @@ -12,7 +12,7 @@ jobs: name: Test and upload artifacts strategy: matrix: - node-version: [21.x] + node-version: [22.x] steps: - name: Install system dependencies @@ -59,6 +59,18 @@ jobs: working-directory: ./packages/pl-fe/dist run: zip -r pl-fe.zip . + - name: Install pl-api deps + working-directory: ./packages/pl-api + run: yarn install --ignore-scripts + + - name: Build pl-api documentation + working-directory: ./packages/pl-api + run: npx typedoc + + - name: Copy pl-api documentation + working-directory: ./packages/pl-api + run: cp docs ../pl-fe/dist/pl-api-docs -r + - name: Upload Github Pages artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d1762aa9a..33be54684 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,6 @@ "bradlc.vscode-tailwindcss", "stylelint.vscode-stylelint", "wix.vscode-import-cost", - "redhat.vscode-yaml" + "bradlc.vscode-tailwindcss" ] } diff --git a/README.md b/README.md index 68f3587f6..4454a39a2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ +[![GitHub Repo stars](https://img.shields.io/github/stars/mkljczk/pl-fe)](https://github.com/mkljczk/pl-fe) +[![GitHub License](https://img.shields.io/github/license/mkljczk/pl-fe)](https://github.com/mkljczk/pl-fe?tab=AGPL-3.0-1-ov-file#readme) +[![Discord](https://img.shields.io/discord/1279834339470872598)](https://discord.gg/NCZZsqqgUH) + This repo hosts a few of my projects related to the Fediverse client `pl-fe`. This includes: - [pl-fe](./packages/pl-fe/) — a social networking client app forked from Soapbox - [pl-api](./packages/pl-api) — a library for interacting with Mastodon API-compatible servers, focused on support for projects extending the official Mastodon API. It is used by `pl-fe`. -- [pl-hooks](./packages/pl-hooks) — a library including hooks for integrating with Mastodon API, based on `pl-api` and TanStack. It is intended to be used within `pl-fe`. Work in progress. +- [pl-hooks](./packages/pl-hooks) — a library including hooks for integrating with Mastodon API, based on `pl-api` and TanStack Query. It is intended to be used within `pl-fe`. Work in progress. More projects to be announced. diff --git a/packages/pl-api/.gitignore b/packages/pl-api/.gitignore index 312c14a36..cfe8c61d7 100644 --- a/packages/pl-api/.gitignore +++ b/packages/pl-api/.gitignore @@ -12,6 +12,7 @@ lerna-debug.log* node_modules dist dist-ssr +docs *.local .idea diff --git a/packages/pl-api/README.md b/packages/pl-api/README.md index 5ef407be2..076455420 100644 --- a/packages/pl-api/README.md +++ b/packages/pl-api/README.md @@ -1,16 +1,48 @@ # `pl-api` -A library for interacting with Mastodon API-compatible servers, focused on support for projects extending the official Mastodon API. +[![GitHub License](https://img.shields.io/github/license/mkljczk/pl-fe)](https://github.com/mkljczk/pl-fe?tab=AGPL-3.0-1-ov-file#readme) +[![NPM Version](https://img.shields.io/npm/v/pl-api) +![NPM Downloads](https://img.shields.io/npm/dw/pl-api)](https://www.npmjs.com/package/pl-api) + +A JavaScript library for interacting with Mastodon API-compatible servers, focused on support for projects extending the official Mastodon API. + +`pl-api` attempts to abstract out the implementation details when supporting different backends, implementing the same features in different ways. It uses [Valibot](https://valibot.dev/) to ensure type safety and normalize API responses. + +Example: +```ts +import { PlApiClient, type CreateApplicationParams } from 'pl-api'; + +const { ACCESS_TOKEN } = process.env; + +const client = new PlApiClient('https://mastodon.example/', ACCESS_TOKEN, { + fetchInstance: true, + onInstanceFetchSuccess: () => console.log('Instance fetched'), +}); + +await client.statuses.createStatus({ + status: 'Hello, world!', + language: 'en', +}); +``` + +Some sort of documentation is available on https://pl.mkljczk.pl/pl-api-docs > This project should be considered unstable before the 1.0.0 release. I will not provide any changelog or information on breaking changes until then. +## Supported projects + +Currently, `pl-api` includes compatibility definitions for 12 independent Mastodon API implementations and 5 variants of them (like, forks). As the combination of software name and version is not the only way `pl-api` infers feature availability, some feature definitions will also work on other software. + +For unsupported projects, it falls back to a basic feature set, though every method of PlApiClient may be used anyway. + ## Projects using `pl-api` -[`pl-fe`](https://github.com/mkljczk/pl-fe) is a web client for Mastodon-compatible servers forked from Soapbox. It uses `pl-api` for API interactions. +* [`pl-fe`](https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-fe) is a web client for Mastodon-compatible servers forked from Soapbox. It uses `pl-api` for API interactions. +* [`pl-hooks`](https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-hooks) is a work-in-progress library utilizing `pl-api`. ## License -`pl-api` utilizes code from [Soapbox](https://gitlab.com/soapbox-pub/soapbox) and bases off official [Mastodon documentation](https://docs.joinmastodon.org). +`pl-api` utilizes parts of code from [Soapbox](https://gitlab.com/soapbox-pub/soapbox) and bases off official [Mastodon documentation](https://docs.joinmastodon.org). This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 22f9413d5..005934cd9 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -1,3 +1,6 @@ +import { WebSocket } from 'isows'; +import omit from 'lodash.omit'; +import pick from 'lodash.pick'; import * as v from 'valibot'; import { @@ -27,6 +30,7 @@ import { contextSchema, conversationSchema, credentialAccountSchema, + credentialApplicationSchema, customEmojiSchema, domainBlockSchema, emojiReactionSchema, @@ -70,22 +74,140 @@ import { trendsLinkSchema, webPushSubscriptionSchema, } from './entities'; +import { GroupedNotificationsResults, groupedNotificationsResultsSchema, NotificationGroup } from './entities/grouped-notifications-results'; import { filteredArray } from './entities/utils'; import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA } from './features'; +import { + CreateScrobbleParams, + FollowAccountParams, + GetAccountEndorsementsParams, + GetAccountFavouritesParams, + GetAccountFollowersParams, + GetAccountFollowingParams, + GetAccountParams, + GetAccountStatusesParams, + GetRelationshipsParams, + GetScrobblesParams, + ReportAccountParams, + SearchAccountParams, +} from './params/accounts'; +import { CreateApplicationParams } from './params/apps'; +import { + CreateChatMessageParams, + GetChatMessagesParams, + GetChatsParams, +} from './params/chats'; +import { + CreateEventParams, + EditEventParams, + GetEventParticipationRequestsParams, + GetEventParticipationsParams, + GetJoinedEventsParams, +} from './params/events'; +import { + CreateFilterParams, + GetBlocksParams, + GetDomainBlocksParams, + GetMutesParams, + MuteAccountParams, + UpdateFilterParams, +} from './params/filtering'; +import { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams } from './params/grouped-notifications'; +import { + CreateGroupParams, + GetGroupBlocksParams, + GetGroupMembershipRequestsParams, + GetGroupMembershipsParams, + UpdateGroupParams, +} from './params/groups'; +import { ProfileDirectoryParams } from './params/instance'; +import { + GetInteractionRequestsParams, +} from './params/interaction-requests'; +import { + CreateListParams, + GetListAccountsParams, + UpdateListParams, +} from './params/lists'; +import { + UpdateMediaParams, + UploadMediaParams, +} from './params/media'; +import { + CreateBookmarkFolderParams, + GetBookmarksParams, + GetEndorsementsParams, + GetFavouritesParams, + GetFollowedTagsParams, + GetFollowRequestsParams, + UpdateBookmarkFolderParams, +} from './params/my-account'; +import { + GetNotificationParams, + GetNotificationRequestsParams, + GetUnreadNotificationCountParams, + UpdateNotificationPolicyRequest, +} from './params/notifications'; +import { + GetTokenParams, + MfaChallengeParams, + OauthAuthorizeParams, + RevokeTokenParams, +} from './params/oauth'; +import { + CreatePushNotificationsSubscriptionParams, + UpdatePushNotificationsSubscriptionParams, +} from './params/push-notifications'; +import { GetScheduledStatusesParams } from './params/scheduled-statuses'; +import { SearchParams } from './params/search'; +import { + CreateAccountParams, + UpdateCredentialsParams, + UpdateInteractionPoliciesParams, + UpdateNotificationSettingsParams, +} from './params/settings'; +import { + CreateStatusParams, + EditStatusParams, + GetFavouritedByParams, + GetRebloggedByParams, + GetStatusContextParams, + GetStatusesParams, + GetStatusParams, + GetStatusQuotesParams, +} from './params/statuses'; +import { + BubbleTimelineParams, + GetConversationsParams, + GroupTimelineParams, + HashtagTimelineParams, + HomeTimelineParams, + ListTimelineParams, + PublicTimelineParams, + SaveMarkersParams, +} from './params/timelines'; +import { + GetTrendingLinks, + GetTrendingStatuses, + GetTrendingTags, +} from './params/trends'; import request, { getNextLink, getPrevLink, type RequestBody, RequestMeta } from './request'; import { buildFullPath } from './utils/url'; import type { + Account, AdminAccount, AdminAnnouncement, AdminModerationLogEntry, AdminReport, GroupRole, Instance, + Notification, PleromaConfig, Status, StreamingEvent, } from './entities'; +import type { PlApiResponse } from './main'; import type { AdminAccountAction, AdminCreateAnnouncementParams, @@ -114,89 +236,29 @@ import type { AdminUpdateReportParams, AdminUpdateRuleParams, AdminUpdateStatusParams, - BubbleTimelineParams, - CreateAccountParams, - CreateApplicationParams, - CreateBookmarkFolderParams, - CreateChatMessageParams, - CreateEventParams, - CreateFilterParams, - CreateGroupParams, - CreateListParams, - CreatePushNotificationsSubscriptionParams, - CreateScrobbleParams, - CreateStatusParams, - EditEventParams, - EditStatusParams, - FollowAccountParams, - GetAccountEndorsementsParams, - GetAccountFavouritesParams, - GetAccountFollowersParams, - GetAccountFollowingParams, - GetAccountParams, - GetAccountStatusesParams, - GetBlocksParams, - GetBookmarksParams, - GetChatMessagesParams, - GetChatsParams, - GetConversationsParams, - GetDomainBlocksParams, - GetEndorsementsParams, - GetEventParticipationRequestsParams, - GetEventParticipationsParams, - GetFavouritedByParams, - GetFavouritesParams, - GetFollowedTagsParams, - GetFollowRequestsParams, - GetGroupBlocksParams, - GetGroupMembershipRequestsParams, - GetGroupMembershipsParams, - GetInteractionRequestsParams, - GetJoinedEventsParams, - GetListAccountsParams, - GetMutesParams, - GetNotificationParams, - GetNotificationRequestsParams, - GetRebloggedByParams, - GetRelationshipsParams, - GetScheduledStatusesParams, - GetScrobblesParams, - GetStatusContextParams, - GetStatusesParams, - GetStatusParams, - GetStatusQuotesParams, - GetTokenParams, - GetTrendingLinks, - GetTrendingStatuses, - GetTrendingTags, - GroupTimelineParams, - HashtagTimelineParams, - HomeTimelineParams, - ListTimelineParams, - MfaChallengeParams, - MuteAccountParams, - OauthAuthorizeParams, - ProfileDirectoryParams, - PublicTimelineParams, - ReportAccountParams, - RevokeTokenParams, - SaveMarkersParams, - SearchAccountParams, - SearchParams, - UpdateBookmarkFolderParams, - UpdateCredentialsParams, - UpdateFilterParams, - UpdateGroupParams, - UpdateInteractionPoliciesParams, - UpdateListParams, - UpdateMediaParams, - UpdateNotificationPolicyRequest, - UpdateNotificationSettingsParams, - UpdatePushNotificationsSubscriptionParams, - UploadMediaParams, -} from './params'; +} from './params/admin'; import type { PaginatedResponse } from './responses'; +const GROUPED_TYPES = ['favourite', 'reblog', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request']; + +interface PlApiClientConstructorOpts { + /** Instance object to use by default, to be populated eg. from cache */ + instance?: Instance; + /** 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; + /** Executed when the initial instance fetch failed */ + onInstanceFetchError?: (error?: any) => void; +} + +/** + * @category Clients + * + * Mastodon API client. + */ class PlApiClient { baseURL: string; @@ -209,13 +271,21 @@ class PlApiClient { unlisten: (listener: any) => void; subscribe: (stream: string, params?: { list?: string; tag?: string }) => void; unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void; - close: () => void; + close: () => void; }; - constructor(baseURL: string, accessToken?: string, { instance, fetchInstance }: { - instance?: Instance; - fetchInstance?: boolean; - } = {}) { + /** + * + * @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, + }: PlApiClientConstructorOpts = {}) { this.baseURL = baseURL; this.#accessToken = accessToken; @@ -223,30 +293,31 @@ class PlApiClient { this.#setInstance(instance); } if (fetchInstance) { - this.instance.getInstance(); + this.instance.getInstance().then((instance) => { + if (fetchInstanceSignal?.aborted) return; + onInstanceFetchSuccess?.(instance); + }).catch((error) => { + if (fetchInstanceSignal?.aborted) return; + onInstanceFetchError?.(error); + }); } } - #paginatedGet = async (input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema>): Promise> => { - const getMore = (input: string | null) => input ? async () => { - const response = await this.request(input); + #paginatedGet = async (input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema>, isArray = true as IsArray): Promise> => { + const targetSchema = isArray ? filteredArray(schema) : schema; - return { - previous: getMore(getPrevLink(response)), - next: getMore(getNextLink(response)), - items: v.parse(filteredArray(schema), response.json), - partial: response.status === 206, - }; - } : null; + 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 { - previous: getMore(getPrevLink(response)), - next: getMore(getNextLink(response)), - items: v.parse(filteredArray(schema), response.json), - partial: response.status === 206, - }; + return processResponse(response); }; #paginatedPleromaAccounts = async (params: { @@ -310,6 +381,65 @@ class PlApiClient { }; }; + #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-ignore + && 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-ignore + status_id: notification.status?.id, + // @ts-ignore + 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) 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 = { /** @@ -320,7 +450,7 @@ class PlApiClient { createApplication: async (params: CreateApplicationParams) => { const response = await this.request('/api/v1/apps', { method: 'POST', body: params }); - return v.parse(applicationSchema, response.json); + return v.parse(credentialApplicationSchema, response.json); }, /** @@ -910,7 +1040,7 @@ class PlApiClient { * 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` + * Requires features{@link Features['accountCreation']} * @see {@link https://docs.joinmastodon.org/methods/accounts/#create} */ createAccount: async (params: CreateAccountParams) => { @@ -1216,7 +1346,7 @@ class PlApiClient { mfa: { /** - * Requires features{@link Features['manageMfa`. + * Requires features{@link Features['manageMfa']}. */ getMfaSettings: async () => { const response = await this.request('/api/pleroma/accounts/mfa'); @@ -1230,7 +1360,7 @@ class PlApiClient { }, /** - * Requires features{@link Features['manageMfa`. + * Requires features{@link Features['manageMfa']}. */ getMfaBackupCodes: async () => { const response = await this.request('/api/pleroma/accounts/mfa/backup_codes'); @@ -1241,7 +1371,7 @@ class PlApiClient { }, /** - * Requires features{@link Features['manageMfa`. + * Requires features{@link Features['manageMfa']}. */ getMfaSetup: async (method: 'totp') => { const response = await this.request(`/api/pleroma/accounts/mfa/setup/${method}`); @@ -1253,7 +1383,7 @@ class PlApiClient { }, /** - * Requires features{@link Features['manageMfa`. + * Requires features{@link Features['manageMfa']}. */ confirmMfaSetup: async (method: 'totp', code: string, password: string) => { const response = await this.request(`/api/pleroma/accounts/mfa/confirm/${method}`, { @@ -1267,7 +1397,7 @@ class PlApiClient { }, /** - * Requires features{@link Features['manageMfa`. + * Requires features{@link Features['manageMfa']}. */ disableMfa: async (method: 'totp', password: string) => { const response = await this.request(`/api/pleroma/accounts/mfa/${method}`, { @@ -2084,11 +2214,8 @@ class PlApiClient { * Requires features{@link Features['statusDislikes']}. * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#get-apifriendicastatusesiddisliked_by} */ - getDislikedBy: async (statusId: string) => { - const response = await this.request(`/api/friendica/statuses/${statusId}/disliked_by`); - - return v.parse(filteredArray(accountSchema), response.json); - }, + getDislikedBy: async (statusId: string) => + this.#paginatedGet(`/api/v1/statuses/${statusId}/disliked_by`, {}, accountSchema), /** * Marks the given status as disliked by this user @@ -2524,13 +2651,30 @@ class PlApiClient { return response.json as {}; }, + /** + * 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/v1/notifications/policy'); + const response = await this.request('/api/v2/notifications/policy'); return v.parse(notificationPolicySchema, response.json); }, @@ -2538,10 +2682,12 @@ class PlApiClient { /** * 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/v1/notifications/policy', { method: 'POST', body: params }); + const response = await this.request('/api/v2/notifications/policy', { method: 'PATCH', body: params }); return v.parse(notificationPolicySchema, response.json); }, @@ -2587,6 +2733,44 @@ class PlApiClient { return response.json as {}; }, + /** + * 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 as {}; + }, + + /** + * 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 as {}; + }, + + /** + * 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. * @@ -2603,6 +2787,108 @@ class PlApiClient { }, }; + /** + * 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); + } else { + 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); + } else { + 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 as {}; + } else { + 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); + } else { + 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); + } else { + 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 @@ -4051,7 +4337,7 @@ class PlApiClient { public readonly events = { /** * Creates an event - * @see {@link } + * @see {@link https://github.com/mkljczk/pl/blob/fork/docs/development/API/pleroma_api.md#apiv1pleromaevents} */ createEvent: async (params: CreateEventParams) => { const response = await this.request('/api/v1/pleroma/events', { method: 'POST', body: params }); @@ -4061,7 +4347,7 @@ class PlApiClient { /** * Edits an event - * @see {@link } + * @see {@link https://github.com/mkljczk/pl/blob/fork/docs/development/API/pleroma_api.md#apiv1pleromaeventsid} */ editEvent: async (statusId: string, params: EditEventParams) => { const response = await this.request(`/api/v1/pleroma/events/${statusId}`, { method: 'PUT', body: params }); @@ -4071,7 +4357,7 @@ class PlApiClient { /** * Gets user's joined events - * @see {@link } + * @see {@link https://github.com/mkljczk/pl/blob/fork/docs/development/API/pleroma_api.md#apiv1pleromaeventsjoined_events} */ getJoinedEvents: async (state?: 'pending' | 'reject' | 'accept', params?: GetJoinedEventsParams) => this.#paginatedGet('/api/v1/pleroma/events/joined_events', { params: { ...params, state } }, statusSchema), diff --git a/packages/pl-api/lib/directory-client.ts b/packages/pl-api/lib/directory-client.ts index ae11e1b8c..0b280de87 100644 --- a/packages/pl-api/lib/directory-client.ts +++ b/packages/pl-api/lib/directory-client.ts @@ -8,19 +8,36 @@ import { filteredArray } from './entities/utils'; import request from './request'; interface Params { + /** ISO 639 language code for servers. */ language?: string; + /** Server topic. */ category?: string; + /** Region where teh server is legally based. */ region?: 'europe' | 'north_america' | 'south_america' | 'africa' | 'asia' | 'oceania'; + /** Whether the server is governed by a public organization or a private individual. */ ownership?: 'juridicial' | 'natural'; + /** Whether the registrations are currently open. */ registrations?: 'instant' | 'manual'; } +/** + * @category Clients + * + * joinmastodon.org-compatible server directory client. + */ class PlApiDirectoryClient { - accessToken = undefined; + /** Unused. */ + accessToken: string | undefined = undefined; + /** + * Server directory URL. + */ baseURL: string; public request = request.bind(this) as typeof request; + /** + * @param baseURL Server directory URL. e.g. `https://joinmastodon.org` + */ constructor(baseURL: string) { this.baseURL = baseURL; } diff --git a/packages/pl-api/lib/entities/account-warning.ts b/packages/pl-api/lib/entities/account-warning.ts index 57befd64f..9ec3eea5b 100644 --- a/packages/pl-api/lib/entities/account-warning.ts +++ b/packages/pl-api/lib/entities/account-warning.ts @@ -9,7 +9,10 @@ const appealSchema = v.object({ state: v.picklist(['approved', 'rejected', 'pending']), }); -/** @see {@link https://docs.joinmastodon.org/entities/AccountWarning/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/AccountWarning/} +*/ const accountWarningSchema = v.object({ id: v.string(), action: v.picklist(['none', 'disable', 'mark_statuses_as_sensitive', 'delete_statuses', 'sensitive', 'silence', 'suspend']), @@ -20,6 +23,9 @@ const accountWarningSchema = v.object({ created_at: v.fallback(datetimeSchema, new Date().toISOString()), }); +/** + * @category Entity types + */ type AccountWarning = v.InferOutput; export { accountWarningSchema, type AccountWarning }; diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 35ee9c76c..64d8b712d 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -119,7 +119,7 @@ const baseAccountSchema = v.object({ following_count: v.fallback(v.number(), 0), roles: filteredArray(roleSchema), - fqn: v.fallback(v.nullable(v.string()), null), + fqn: v.string(), ap_id: v.fallback(v.nullable(v.string()), null), background_image: v.fallback(v.nullable(v.string()), null), relationship: v.fallback(v.optional(relationshipSchema), undefined), @@ -164,8 +164,14 @@ type WithMoved = { moved: Account | null; }; +/** + * @category Entity types + */ type Account = v.InferOutput & WithMoved; +/** + * @category Schemas + */ const accountSchema: v.BaseSchema> = untypedAccountSchema as any; const untypedCredentialAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ @@ -197,8 +203,14 @@ const untypedCredentialAccountSchema = v.pipe(v.any(), preprocessAccount, v.obje })), undefined), })); +/** + * @category Entity types + */ type CredentialAccount = v.InferOutput & WithMoved; +/** + * @category Schemas + */ const credentialAccountSchema: v.BaseSchema> = untypedCredentialAccountSchema as any; const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ @@ -206,8 +218,14 @@ const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ mute_expires_at: v.fallback(v.nullable(datetimeSchema), null), })); +/** + * @category Entity types + */ type MutedAccount = v.InferOutput & WithMoved; +/** + * @category Schemas + */ const mutedAccountSchema: v.BaseSchema> = untypedMutedAccountSchema as any; export { diff --git a/packages/pl-api/lib/entities/admin/account.ts b/packages/pl-api/lib/entities/admin/account.ts index 7d5274dc6..caf4f233e 100644 --- a/packages/pl-api/lib/entities/admin/account.ts +++ b/packages/pl-api/lib/entities/admin/account.ts @@ -6,7 +6,10 @@ import { datetimeSchema, filteredArray } from '../utils'; import { adminIpSchema } from './ip'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Account/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Account/} + */ const adminAccountSchema = v.pipe( v.any(), v.transform((account: any) => { @@ -65,6 +68,9 @@ const adminAccountSchema = v.pipe( }), ); +/** + * @category Admin entity types + */ type AdminAccount = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/announcement.ts b/packages/pl-api/lib/entities/admin/announcement.ts index 93d9bca26..ef6b20fba 100644 --- a/packages/pl-api/lib/entities/admin/announcement.ts +++ b/packages/pl-api/lib/entities/admin/announcement.ts @@ -3,7 +3,10 @@ import * as v from 'valibot'; import { announcementSchema } from '../announcement'; -/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} */ +/** + * @category Admin schemas + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} + */ const adminAnnouncementSchema = v.pipe( v.any(), v.transform((announcement: any) => ({ @@ -16,6 +19,9 @@ const adminAnnouncementSchema = v.pipe( }), ); +/** + * @category Admin entity types + */ type AdminAnnouncement = v.InferOutput; export { adminAnnouncementSchema, type AdminAnnouncement }; diff --git a/packages/pl-api/lib/entities/admin/canonical-email-block.ts b/packages/pl-api/lib/entities/admin/canonical-email-block.ts index 2394bb44b..1ed511266 100644 --- a/packages/pl-api/lib/entities/admin/canonical-email-block.ts +++ b/packages/pl-api/lib/entities/admin/canonical-email-block.ts @@ -1,11 +1,17 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_CanonicalEmailBlock/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_CanonicalEmailBlock/} + */ const adminCanonicalEmailBlockSchema = v.object({ id: v.string(), canonical_email_hash: v.string(), }); +/** + * @category Admin entity types + */ type AdminCanonicalEmailBlock = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/cohort.ts b/packages/pl-api/lib/entities/admin/cohort.ts index c536f107b..ceaa51641 100644 --- a/packages/pl-api/lib/entities/admin/cohort.ts +++ b/packages/pl-api/lib/entities/admin/cohort.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Cohort/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Cohort/} + */ const adminCohortSchema = v.object({ period: datetimeSchema, frequency: v.picklist(['day', 'month']), @@ -13,6 +16,9 @@ const adminCohortSchema = v.object({ })), }); +/** + * @category Admin entity types + */ type AdminCohort = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/dimension.ts b/packages/pl-api/lib/entities/admin/dimension.ts index b382da6a7..1804f90dc 100644 --- a/packages/pl-api/lib/entities/admin/dimension.ts +++ b/packages/pl-api/lib/entities/admin/dimension.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Dimension/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Dimension/} + */ const adminDimensionSchema = v.object({ key: v.string(), data: v.object({ @@ -12,6 +15,9 @@ const adminDimensionSchema = v.object({ }), }); +/** + * @category Admin entity types + */ type AdminDimension = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/domain-allow.ts b/packages/pl-api/lib/entities/admin/domain-allow.ts index 5024d2b12..109500052 100644 --- a/packages/pl-api/lib/entities/admin/domain-allow.ts +++ b/packages/pl-api/lib/entities/admin/domain-allow.ts @@ -2,13 +2,19 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_DomainAllow/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_DomainAllow/} + */ const adminDomainAllowSchema = v.object({ id: v.string(), domain: v.string(), created_at: datetimeSchema, }); +/** + * @category Admin entity types + */ type AdminDomainAllow = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/domain-block.ts b/packages/pl-api/lib/entities/admin/domain-block.ts index 0dca14241..0a79df86d 100644 --- a/packages/pl-api/lib/entities/admin/domain-block.ts +++ b/packages/pl-api/lib/entities/admin/domain-block.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_DomainBlock/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_DomainBlock/} + */ const adminDomainBlockSchema = v.object({ id: v.string(), domain: v.string(), @@ -16,6 +19,9 @@ const adminDomainBlockSchema = v.object({ obfuscate: v.boolean(), }); +/** + * @category Admin entity types + */ type AdminDomainBlock = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/domain.ts b/packages/pl-api/lib/entities/admin/domain.ts index f0b566670..4a6da0611 100644 --- a/packages/pl-api/lib/entities/admin/domain.ts +++ b/packages/pl-api/lib/entities/admin/domain.ts @@ -2,6 +2,9 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; +/** + * @category Admin schemas + */ const adminDomainSchema = v.object({ domain: v.fallback(v.string(), ''), id: v.pipe(v.unknown(), v.transform(String)), @@ -10,6 +13,9 @@ const adminDomainSchema = v.object({ last_checked_at: v.fallback(v.nullable(datetimeSchema), null), }); +/** + * @category Admin entity types + */ type AdminDomain = v.InferOutput export { adminDomainSchema, type AdminDomain }; diff --git a/packages/pl-api/lib/entities/admin/email-domain-block.ts b/packages/pl-api/lib/entities/admin/email-domain-block.ts index 724f44bb0..f731574be 100644 --- a/packages/pl-api/lib/entities/admin/email-domain-block.ts +++ b/packages/pl-api/lib/entities/admin/email-domain-block.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_EmailDomainBlock/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_EmailDomainBlock/} + */ const adminEmailDomainBlockSchema = v.object({ id: v.string(), domain: v.string(), @@ -14,6 +17,9 @@ const adminEmailDomainBlockSchema = v.object({ })), }); +/** + * @category Admin entity types + */ type AdminEmailDomainBlock = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/ip-block.ts b/packages/pl-api/lib/entities/admin/ip-block.ts index 1a3a62108..b76bd685f 100644 --- a/packages/pl-api/lib/entities/admin/ip-block.ts +++ b/packages/pl-api/lib/entities/admin/ip-block.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_IpBlock/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_IpBlock/} + */ const adminIpBlockSchema = v.object({ id: v.string(), ip: v.pipe(v.string(), v.ip()), @@ -12,6 +15,9 @@ const adminIpBlockSchema = v.object({ expires_at: v.fallback(v.nullable(datetimeSchema), null), }); +/** + * @category Admin entity types + */ type AdminIpBlock = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/ip.ts b/packages/pl-api/lib/entities/admin/ip.ts index f1adce518..1a323a144 100644 --- a/packages/pl-api/lib/entities/admin/ip.ts +++ b/packages/pl-api/lib/entities/admin/ip.ts @@ -2,12 +2,18 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Ip/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Ip/} + */ const adminIpSchema = v.object({ ip: v.pipe(v.string(), v.ip()), used_at: datetimeSchema, }); +/** + * @category Admin entity types + */ type AdminIp = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/measure.ts b/packages/pl-api/lib/entities/admin/measure.ts index fed8a988f..f8b1d85ca 100644 --- a/packages/pl-api/lib/entities/admin/measure.ts +++ b/packages/pl-api/lib/entities/admin/measure.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from '../utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Measure/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Measure/} + */ const adminMeasureSchema = v.object({ key: v.string(), unit: v.fallback(v.nullable(v.string()), null), @@ -15,6 +18,9 @@ const adminMeasureSchema = v.object({ })), }); +/** + * @category Admin entity types + */ type AdminMeasure = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/admin/moderation-log-entry.ts b/packages/pl-api/lib/entities/admin/moderation-log-entry.ts index 81300d5c3..5c36c6b1e 100644 --- a/packages/pl-api/lib/entities/admin/moderation-log-entry.ts +++ b/packages/pl-api/lib/entities/admin/moderation-log-entry.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminmoderation_log} */ +/** + * @category Admin schemas + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminmoderation_log} + */ const adminModerationLogEntrySchema = v.object({ id: v.pipe(v.unknown(), v.transform(String)), data: v.fallback(v.record(v.string(), v.any()), {}), @@ -8,6 +11,9 @@ const adminModerationLogEntrySchema = v.object({ message: v.fallback(v.string(), ''), }); +/** + * @category Admin entity types + */ type AdminModerationLogEntry = v.InferOutput export { adminModerationLogEntrySchema, type AdminModerationLogEntry }; diff --git a/packages/pl-api/lib/entities/admin/pleroma-config.ts b/packages/pl-api/lib/entities/admin/pleroma-config.ts index aee37727a..8621dc3da 100644 --- a/packages/pl-api/lib/entities/admin/pleroma-config.ts +++ b/packages/pl-api/lib/entities/admin/pleroma-config.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Admin schemas + */ const pleromaConfigSchema = v.object({ configs: v.array(v.object({ value: v.any(), @@ -9,6 +12,9 @@ const pleromaConfigSchema = v.object({ need_reboot: v.boolean(), }); +/** + * @category Admin entity types + */ type PleromaConfig = v.InferOutput export { pleromaConfigSchema, type PleromaConfig }; diff --git a/packages/pl-api/lib/entities/admin/relay.ts b/packages/pl-api/lib/entities/admin/relay.ts index e5d4e6993..c2b5ded52 100644 --- a/packages/pl-api/lib/entities/admin/relay.ts +++ b/packages/pl-api/lib/entities/admin/relay.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Admin schemas + */ const adminRelaySchema = v.pipe( v.any(), v.transform((data: any) => ({ id: data.actor, ...data })), @@ -10,6 +13,9 @@ const adminRelaySchema = v.pipe( }), ); +/** + * @category Admin entity types + */ type AdminRelay = v.InferOutput export { adminRelaySchema, type AdminRelay }; diff --git a/packages/pl-api/lib/entities/admin/report.ts b/packages/pl-api/lib/entities/admin/report.ts index 6a0526f75..dffe682fc 100644 --- a/packages/pl-api/lib/entities/admin/report.ts +++ b/packages/pl-api/lib/entities/admin/report.ts @@ -7,7 +7,10 @@ import { datetimeSchema, filteredArray } from '../utils'; import { adminAccountSchema } from './account'; -/** @see {@link https://docs.joinmastodon.org/entities/Admin_Report/} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Admin_Report/} + */ const adminReportSchema = v.pipe( v.any(), v.transform((report: any) => { @@ -45,6 +48,9 @@ const adminReportSchema = v.pipe( }), ); +/** + * @category Admin entity types + */ type AdminReport = v.InferOutput; export { adminReportSchema, type AdminReport }; diff --git a/packages/pl-api/lib/entities/admin/rule.ts b/packages/pl-api/lib/entities/admin/rule.ts index 41838af6a..9173d4a1b 100644 --- a/packages/pl-api/lib/entities/admin/rule.ts +++ b/packages/pl-api/lib/entities/admin/rule.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrules} */ +/** + * @category Admin schemas + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrules} + */ const adminRuleSchema = v.object({ id: v.string(), text: v.fallback(v.string(), ''), @@ -8,6 +11,9 @@ const adminRuleSchema = v.object({ priority: v.fallback(v.nullable(v.number()), null), }); +/** + * @category Admin entity types + */ type AdminRule = v.InferOutput; export { adminRuleSchema, type AdminRule }; diff --git a/packages/pl-api/lib/entities/admin/tag.ts b/packages/pl-api/lib/entities/admin/tag.ts index aaa2818f6..e4320fc01 100644 --- a/packages/pl-api/lib/entities/admin/tag.ts +++ b/packages/pl-api/lib/entities/admin/tag.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { tagSchema } from '../tag'; -/** @see {@link https://docs.joinmastodon.org/entities/Tag/#admin} */ +/** + * @category Admin schemas + * @see {@link https://docs.joinmastodon.org/entities/Tag/#admin} + */ const adminTagSchema = v.object({ ...tagSchema.entries, id: v.string(), @@ -11,6 +14,9 @@ const adminTagSchema = v.object({ requires_review: v.boolean(), }); +/** + * @category Admin entity types + */ type AdminTag = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/announcement-reaction.ts b/packages/pl-api/lib/entities/announcement-reaction.ts index fcb5881d9..0d7217dcc 100644 --- a/packages/pl-api/lib/entities/announcement-reaction.ts +++ b/packages/pl-api/lib/entities/announcement-reaction.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/announcement/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/announcement/} + */ const announcementReactionSchema = v.object({ name: v.fallback(v.string(), ''), count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0), @@ -10,6 +13,9 @@ const announcementReactionSchema = v.object({ announcement_id: v.fallback(v.string(), ''), }); +/** + * @category Entity types + */ type AnnouncementReaction = v.InferOutput; export { announcementReactionSchema, type AnnouncementReaction }; diff --git a/packages/pl-api/lib/entities/announcement.ts b/packages/pl-api/lib/entities/announcement.ts index 5830f3de3..683aba701 100644 --- a/packages/pl-api/lib/entities/announcement.ts +++ b/packages/pl-api/lib/entities/announcement.ts @@ -6,7 +6,10 @@ import { mentionSchema } from './mention'; import { tagSchema } from './tag'; import { datetimeSchema, filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/announcement/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/announcement/} + */ const announcementSchema = v.object({ id: v.string(), content: v.fallback(v.string(), ''), @@ -29,6 +32,9 @@ const announcementSchema = v.object({ updated_at: v.fallback(datetimeSchema, new Date().toISOString()), }); +/** + * @category Entity types + */ type Announcement = v.InferOutput; export { announcementSchema, type Announcement }; diff --git a/packages/pl-api/lib/entities/application.ts b/packages/pl-api/lib/entities/application.ts index 6d3949378..21e8c0059 100644 --- a/packages/pl-api/lib/entities/application.ts +++ b/packages/pl-api/lib/entities/application.ts @@ -1,19 +1,47 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Application/} */ -const applicationSchema = v.object({ +import { filteredArray } from './utils'; + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Application/} + */ +const applicationSchema = v.pipe(v.any(), v.transform((application) => ({ + redirect_uris: [application.redirect_uri], + ...application, +})), v.object({ name: v.fallback(v.string(), ''), website: v.fallback(v.optional(v.string()), undefined), - client_id: v.fallback(v.optional(v.string()), undefined), - client_secret: v.fallback(v.optional(v.string()), undefined), - redirect_uri: v.fallback(v.optional(v.string()), undefined), + redirect_uris: filteredArray(v.string()), id: v.fallback(v.optional(v.string()), undefined), + /** @deprecated */ + redirect_uri: v.fallback(v.optional(v.string()), undefined), /** @deprecated */ vapid_key: v.fallback(v.optional(v.string()), undefined), -}); +})); type Application = v.InferOutput; -export { applicationSchema, type Application }; +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Application/#CredentialApplication} + */ +const credentialApplicationSchema = v.pipe( + applicationSchema.pipe[0], + applicationSchema.pipe[1], + v.object({ + ...applicationSchema.pipe[2].entries, + client_id: v.string(), + client_secret: v.string(), + client_secret_expires_at: v.fallback(v.optional(v.string()), undefined), + }), +); + +/** + * @category Entity types + */ +type CredentialApplication = v.InferOutput; + +export { applicationSchema, credentialApplicationSchema, type Application, type CredentialApplication }; diff --git a/packages/pl-api/lib/entities/backup.ts b/packages/pl-api/lib/entities/backup.ts index fa5008f37..5a142c07e 100644 --- a/packages/pl-api/lib/entities/backup.ts +++ b/packages/pl-api/lib/entities/backup.ts @@ -2,16 +2,22 @@ import * as v from 'valibot'; import { datetimeSchema, mimeSchema } from './utils'; -/** @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromabackups} */ +/** + * @category Schemas + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromabackups} + */ const backupSchema = v.object({ id: v.pipe(v.unknown(), v.transform(String)), - contentType: mimeSchema, + content_type: mimeSchema, file_size: v.fallback(v.number(), 0), inserted_at: datetimeSchema, processed: v.fallback(v.boolean(), false), url: v.fallback(v.string(), ''), }); +/** + * @category Entity types + */ type Backup = v.InferOutput; export { backupSchema, type Backup }; diff --git a/packages/pl-api/lib/entities/bookmark-folder.ts b/packages/pl-api/lib/entities/bookmark-folder.ts index d07c78004..e4e063f48 100644 --- a/packages/pl-api/lib/entities/bookmark-folder.ts +++ b/packages/pl-api/lib/entities/bookmark-folder.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Schemas + */ const bookmarkFolderSchema = v.object({ id: v.pipe(v.unknown(), v.transform(String)), name: v.fallback(v.string(), ''), @@ -7,6 +10,9 @@ const bookmarkFolderSchema = v.object({ emoji_url: v.fallback(v.nullable(v.string()), null), }); +/** + * @category Entity types + */ type BookmarkFolder = v.InferOutput; export { bookmarkFolderSchema, type BookmarkFolder }; diff --git a/packages/pl-api/lib/entities/chat-message.ts b/packages/pl-api/lib/entities/chat-message.ts index 59ca3d39d..3da2f5dcf 100644 --- a/packages/pl-api/lib/entities/chat-message.ts +++ b/packages/pl-api/lib/entities/chat-message.ts @@ -5,7 +5,10 @@ import { mediaAttachmentSchema } from './media-attachment'; import { previewCardSchema } from './preview-card'; import { datetimeSchema, filteredArray } from './utils'; -/** @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-the-messages-for-a-chat} */ +/** + * @category Schemas + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-the-messages-for-a-chat} + */ const chatMessageSchema = v.object({ id: v.string(), content: v.fallback(v.string(), ''), @@ -18,6 +21,9 @@ const chatMessageSchema = v.object({ card: v.fallback(v.nullable(previewCardSchema), null), }); +/** + * @category Entity types + */ type ChatMessage = v.InferOutput; export { chatMessageSchema, type ChatMessage }; diff --git a/packages/pl-api/lib/entities/chat.ts b/packages/pl-api/lib/entities/chat.ts index a7597d82b..ee107809c 100644 --- a/packages/pl-api/lib/entities/chat.ts +++ b/packages/pl-api/lib/entities/chat.ts @@ -4,7 +4,10 @@ import { accountSchema } from './account'; import { chatMessageSchema } from './chat-message'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-a-list-of-chats} */ +/** + * @category Schemas + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-a-list-of-chats} + */ const chatSchema = v.object({ id: v.string(), account: accountSchema, @@ -13,6 +16,9 @@ const chatSchema = v.object({ updated_at: datetimeSchema, }); +/** + * @category Entity types + */ type Chat = v.InferOutput; export { chatSchema, type Chat }; diff --git a/packages/pl-api/lib/entities/context.ts b/packages/pl-api/lib/entities/context.ts index 37449629b..bec10638d 100644 --- a/packages/pl-api/lib/entities/context.ts +++ b/packages/pl-api/lib/entities/context.ts @@ -2,12 +2,18 @@ import * as v from 'valibot'; import { statusSchema } from './status'; -/** @see {@link https://docs.joinmastodon.org/entities/Context/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Context/} + */ const contextSchema = v.object({ ancestors: v.array(statusSchema), descendants: v.array(statusSchema), }); +/** + * @category Entity types + */ type Context = v.InferOutput; export { contextSchema, type Context }; diff --git a/packages/pl-api/lib/entities/conversation.ts b/packages/pl-api/lib/entities/conversation.ts index 50cea95bb..9c806f266 100644 --- a/packages/pl-api/lib/entities/conversation.ts +++ b/packages/pl-api/lib/entities/conversation.ts @@ -4,7 +4,10 @@ import { accountSchema } from './account'; import { statusSchema } from './status'; import { filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Conversation} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Conversation} + */ const conversationSchema = v.object({ id: v.string(), unread: v.fallback(v.boolean(), false), @@ -12,6 +15,9 @@ const conversationSchema = v.object({ last_status: v.fallback(v.nullable(statusSchema), null), }); +/** + * @category Entity types + */ type Conversation = v.InferOutput; export { conversationSchema, type Conversation }; diff --git a/packages/pl-api/lib/entities/custom-emoji.ts b/packages/pl-api/lib/entities/custom-emoji.ts index 5723990a7..a373ff6f0 100644 --- a/packages/pl-api/lib/entities/custom-emoji.ts +++ b/packages/pl-api/lib/entities/custom-emoji.ts @@ -2,6 +2,8 @@ import * as v from 'valibot'; /** * Represents a custom emoji. + * + * @category Schemas * @see {@link https://docs.joinmastodon.org/entities/CustomEmoji/} */ const customEmojiSchema = v.object({ @@ -12,6 +14,9 @@ const customEmojiSchema = v.object({ category: v.fallback(v.nullable(v.string()), null), }); +/** + * @category Entity types + */ type CustomEmoji = v.InferOutput; export { customEmojiSchema, type CustomEmoji }; diff --git a/packages/pl-api/lib/entities/directory/category.ts b/packages/pl-api/lib/entities/directory/category.ts index c9dbca386..68d18d641 100644 --- a/packages/pl-api/lib/entities/directory/category.ts +++ b/packages/pl-api/lib/entities/directory/category.ts @@ -1,10 +1,16 @@ import * as v from 'valibot'; +/** + * @category Directory schemas + */ const directoryCategorySchema = v.object({ category: v.string(), servers_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null), }); +/** + * @category Directory entity types + */ type DirectoryCategory = v.InferOutput; export { directoryCategorySchema, type DirectoryCategory }; diff --git a/packages/pl-api/lib/entities/directory/language.ts b/packages/pl-api/lib/entities/directory/language.ts index 346873325..3b8fd0bf2 100644 --- a/packages/pl-api/lib/entities/directory/language.ts +++ b/packages/pl-api/lib/entities/directory/language.ts @@ -1,11 +1,17 @@ import * as v from 'valibot'; +/** + * @category Directory schemas + */ const directoryLanguageSchema = v.object({ locale: v.string(), language: v.string(), servers_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null), }); +/** + * @category Directory entity types + */ type DirectoryLanguage = v.InferOutput; export { directoryLanguageSchema, type DirectoryLanguage }; diff --git a/packages/pl-api/lib/entities/directory/server.ts b/packages/pl-api/lib/entities/directory/server.ts index ef8ea1b12..2c6ed8255 100644 --- a/packages/pl-api/lib/entities/directory/server.ts +++ b/packages/pl-api/lib/entities/directory/server.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Directory schemas + */ const directoryServerSchema = v.object({ domain: v.string(), version: v.string(), @@ -16,6 +19,9 @@ const directoryServerSchema = v.object({ category: v.string(), }); +/** + * @category Directory entity types + */ type DirectoryServer = v.InferOutput; export { directoryServerSchema, type DirectoryServer }; diff --git a/packages/pl-api/lib/entities/directory/statistics-period.ts b/packages/pl-api/lib/entities/directory/statistics-period.ts index ea37a321d..15dcb2c82 100644 --- a/packages/pl-api/lib/entities/directory/statistics-period.ts +++ b/packages/pl-api/lib/entities/directory/statistics-period.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Directory schemas + */ const directoryStatisticsPeriodSchema = v.object({ period: v.pipe(v.string(), v.isoDate()), server_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null), @@ -7,6 +10,9 @@ const directoryStatisticsPeriodSchema = v.object({ active_user_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null), }); +/** + * @category Directory entity types + */ type DirectoryStatisticsPeriod = v.InferOutput; export { directoryStatisticsPeriodSchema, type DirectoryStatisticsPeriod }; diff --git a/packages/pl-api/lib/entities/domain-block.ts b/packages/pl-api/lib/entities/domain-block.ts index e5bf6e0da..c5f469d9e 100644 --- a/packages/pl-api/lib/entities/domain-block.ts +++ b/packages/pl-api/lib/entities/domain-block.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/DomainBlock} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/DomainBlock} + */ const domainBlockSchema = v.object({ domain: v.string(), digest: v.string(), @@ -8,6 +11,9 @@ const domainBlockSchema = v.object({ comment: v.fallback(v.optional(v.string()), undefined), }); +/** + * @category Entity types + */ type DomainBlock = v.InferOutput; export { domainBlockSchema, type DomainBlock }; diff --git a/packages/pl-api/lib/entities/emoji-reaction.ts b/packages/pl-api/lib/entities/emoji-reaction.ts index 2fbccf22d..f2568a7fb 100644 --- a/packages/pl-api/lib/entities/emoji-reaction.ts +++ b/packages/pl-api/lib/entities/emoji-reaction.ts @@ -22,6 +22,8 @@ const customEmojiReactionSchema = v.object({ /** * Pleroma emoji reaction. + * + * @category Schemas * @see {@link https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#statuses} */ const emojiReactionSchema = v.pipe( @@ -34,6 +36,9 @@ const emojiReactionSchema = v.pipe( v.union([baseEmojiReactionSchema, customEmojiReactionSchema]), ); +/** + * @category Entity types + */ type EmojiReaction = v.InferOutput; export { emojiReactionSchema, type EmojiReaction }; diff --git a/packages/pl-api/lib/entities/extended-description.ts b/packages/pl-api/lib/entities/extended-description.ts index 934f7a413..4c3b07071 100644 --- a/packages/pl-api/lib/entities/extended-description.ts +++ b/packages/pl-api/lib/entities/extended-description.ts @@ -2,12 +2,18 @@ import * as v from 'valibot'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/ExtendedDescription} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/ExtendedDescription} + */ const extendedDescriptionSchema = v.object({ updated_at: datetimeSchema, content: v.string(), }); +/** + * @category Entity types + */ type ExtendedDescription = v.InferOutput; export { extendedDescriptionSchema, type ExtendedDescription }; diff --git a/packages/pl-api/lib/entities/familiar-followers.ts b/packages/pl-api/lib/entities/familiar-followers.ts index ddf1e5c91..519f9fe48 100644 --- a/packages/pl-api/lib/entities/familiar-followers.ts +++ b/packages/pl-api/lib/entities/familiar-followers.ts @@ -3,12 +3,18 @@ import * as v from 'valibot'; import { accountSchema } from './account'; import { filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/FamiliarFollowers/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/FamiliarFollowers/} + */ const familiarFollowersSchema = v.object({ id: v.string(), accounts: filteredArray(accountSchema), }); +/** + * @category Entity types + */ type FamiliarFollowers = v.InferOutput export { familiarFollowersSchema, type FamiliarFollowers }; diff --git a/packages/pl-api/lib/entities/featured-tag.ts b/packages/pl-api/lib/entities/featured-tag.ts index f07ad3e7b..14f9730e8 100644 --- a/packages/pl-api/lib/entities/featured-tag.ts +++ b/packages/pl-api/lib/entities/featured-tag.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/FeaturedTag/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/FeaturedTag/} + */ const featuredTagSchema = v.object({ id: v.string(), name: v.string(), @@ -9,6 +12,9 @@ const featuredTagSchema = v.object({ last_status_at: v.number(), }); +/** + * @category Entity types + */ type FeaturedTag = v.InferOutput; export { featuredTagSchema, type FeaturedTag }; diff --git a/packages/pl-api/lib/entities/filter-result.ts b/packages/pl-api/lib/entities/filter-result.ts index fcd64be77..9b8c37eb1 100644 --- a/packages/pl-api/lib/entities/filter-result.ts +++ b/packages/pl-api/lib/entities/filter-result.ts @@ -2,13 +2,19 @@ import * as v from 'valibot'; import { filterSchema } from './filter'; -/** @see {@link https://docs.joinmastodon.org/entities/FilterResult/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/FilterResult/} + */ const filterResultSchema = v.object({ filter: filterSchema, keyword_matches: v.fallback(v.nullable(v.string()), null), status_matches: v.fallback(v.nullable(v.string()), null), }); +/** + * @category Entity types + */ type FilterResult = v.InferOutput; export { filterResultSchema, type FilterResult }; diff --git a/packages/pl-api/lib/entities/filter.ts b/packages/pl-api/lib/entities/filter.ts index 07c898f2b..ca41bfe49 100644 --- a/packages/pl-api/lib/entities/filter.ts +++ b/packages/pl-api/lib/entities/filter.ts @@ -2,20 +2,29 @@ import * as v from 'valibot'; import { datetimeSchema, filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/FilterKeyword/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/FilterKeyword/} + */ const filterKeywordSchema = v.object({ id: v.string(), keyword: v.string(), whole_word: v.boolean(), }); -/** @see {@link https://docs.joinmastodon.org/entities/FilterStatus/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/FilterStatus/} + */ const filterStatusSchema = v.object({ id: v.string(), status_id: v.string(), }); -/** @see {@link https://docs.joinmastodon.org/entities/Filter/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Filter/} + */ const filterSchema = v.pipe( v.any(), v.transform((filter: any) => { @@ -44,6 +53,9 @@ const filterSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Filter = v.InferOutput; export { filterKeywordSchema, filterStatusSchema, filterSchema, type Filter }; diff --git a/packages/pl-api/lib/entities/group-member.ts b/packages/pl-api/lib/entities/group-member.ts index 7f4f80f26..8919af00a 100644 --- a/packages/pl-api/lib/entities/group-member.ts +++ b/packages/pl-api/lib/entities/group-member.ts @@ -8,14 +8,23 @@ enum GroupRoles { USER = 'user' } +/** + * @category Entity types + */ type GroupRole =`${GroupRoles}`; +/** + * @category Schemas + */ const groupMemberSchema = v.object({ id: v.string(), account: accountSchema, role: v.enum(GroupRoles), }); +/** + * @category Entity types + */ type GroupMember = v.InferOutput; export { groupMemberSchema, type GroupMember, GroupRoles, type GroupRole }; diff --git a/packages/pl-api/lib/entities/group-relationship.ts b/packages/pl-api/lib/entities/group-relationship.ts index 96c2a0436..2ef313133 100644 --- a/packages/pl-api/lib/entities/group-relationship.ts +++ b/packages/pl-api/lib/entities/group-relationship.ts @@ -2,6 +2,9 @@ import * as v from 'valibot'; import { GroupRoles } from './group-member'; +/** + * @category Schemas + */ const groupRelationshipSchema = v.object({ id: v.string(), member: v.fallback(v.boolean(), false), @@ -9,6 +12,9 @@ const groupRelationshipSchema = v.object({ requested: v.fallback(v.boolean(), false), }); +/** + * @category Entity types + */ type GroupRelationship = v.InferOutput; export { groupRelationshipSchema, type GroupRelationship }; diff --git a/packages/pl-api/lib/entities/group.ts b/packages/pl-api/lib/entities/group.ts index 47b413e0e..60e2f00ea 100644 --- a/packages/pl-api/lib/entities/group.ts +++ b/packages/pl-api/lib/entities/group.ts @@ -4,6 +4,9 @@ import { customEmojiSchema } from './custom-emoji'; import { groupRelationshipSchema } from './group-relationship'; import { datetimeSchema, filteredArray } from './utils'; +/** + * @category Schemas + */ const groupSchema = v.object({ avatar: v.fallback(v.string(), ''), avatar_static: v.fallback(v.string(), ''), @@ -28,6 +31,9 @@ const groupSchema = v.object({ header_description: v.fallback(v.string(), ''), }); +/** + * @category Entity types + */ type Group = v.InferOutput; export { groupSchema, type Group }; diff --git a/packages/pl-api/lib/entities/grouped-notifications-results.ts b/packages/pl-api/lib/entities/grouped-notifications-results.ts new file mode 100644 index 000000000..ff5437f33 --- /dev/null +++ b/packages/pl-api/lib/entities/grouped-notifications-results.ts @@ -0,0 +1,157 @@ +import pick from 'lodash.pick'; +import * as v from 'valibot'; + +import { accountSchema } from './account'; +import { accountWarningSchema } from './account-warning'; +import { chatMessageSchema } from './chat-message'; +import { relationshipSeveranceEventSchema } from './relationship-severance-event'; +import { reportSchema } from './report'; +import { statusSchema } from './status'; +import { datetimeSchema, filteredArray } from './utils'; + +const partialAccountWithAvatarSchema = v.object({ + id: v.string(), + acct: v.string(), + url: v.pipe(v.string(), v.url()), + avatar: v.pipe(v.string(), v.url()), + avatar_static: v.pipe(v.string(), v.url()), + locked: v.boolean(), + bot: v.boolean(), +}); + +const baseNotificationGroupSchema = v.object({ + group_key: v.string(), + notifications_count: v.pipe(v.number(), v.integer()), + most_recent_notification_id: v.pipe(v.unknown(), v.transform(String), v.string()), + page_min_id: v.fallback(v.optional(v.string()), undefined), + page_max_id: v.fallback(v.optional(v.string()), undefined), + latest_page_notification_at: v.fallback(v.optional(datetimeSchema), undefined), + sample_account_ids: v.array(v.string()), + + is_muted: v.fallback(v.optional(v.boolean()), undefined), + is_seen: v.fallback(v.optional(v.boolean()), undefined), +}); + +const accountNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.picklist(['follow', 'follow_request', 'admin.sign_up', 'bite']), +}); + +const mentionNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('mention'), + subtype: v.fallback(v.nullable(v.picklist(['reply'])), null), + status_id: v.string(), +}); + +const statusNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.picklist(['status', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']), + status_id: v.string(), +}); + +const reportNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('admin.report'), + report: reportSchema, +}); + +const severedRelationshipNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('severed_relationships'), + relationship_severance_event: relationshipSeveranceEventSchema, +}); + +const moderationWarningNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('moderation_warning'), + moderation_warning: accountWarningSchema, +}); + +const moveNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('move'), + target_id: v.string(), +}); + +const emojiReactionNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('emoji_reaction'), + emoji: v.string(), + emoji_url: v.fallback(v.nullable(v.string()), null), + status_id: v.string(), +}); + +const chatMentionNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('chat_mention'), + chat_message: chatMessageSchema, +}); + +const eventParticipationRequestNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.picklist(['participation_accepted', 'participation_request']), + status_id: v.string(), + participation_message: v.fallback(v.nullable(v.string()), null), +}); + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Notification/} + * */ +const notificationGroupSchema: v.BaseSchema> = v.pipe( + v.any(), + v.transform((notification: any) => ({ + group_key: `ungrouped-${notification.id}`, + ...pick(notification.pleroma || {}, ['is_muted', 'is_seen']), + ...notification, + type: notification.type === 'pleroma:report' + ? 'admin.report' + : notification.type?.replace(/^pleroma:/, ''), + })), + v.variant('type', [ + accountNotificationGroupSchema, + mentionNotificationGroupSchema, + statusNotificationGroupSchema, + reportNotificationGroupSchema, + severedRelationshipNotificationGroupSchema, + moderationWarningNotificationGroupSchema, + moveNotificationGroupSchema, + emojiReactionNotificationGroupSchema, + chatMentionNotificationGroupSchema, + eventParticipationRequestNotificationGroupSchema, + ])) as any; + +/** + * @category Entity types + */ +type NotificationGroup = v.InferOutput< + | typeof accountNotificationGroupSchema + | typeof mentionNotificationGroupSchema + | typeof statusNotificationGroupSchema + | typeof reportNotificationGroupSchema + | typeof severedRelationshipNotificationGroupSchema + | typeof moderationWarningNotificationGroupSchema + | typeof moveNotificationGroupSchema + | typeof emojiReactionNotificationGroupSchema + | typeof chatMentionNotificationGroupSchema + | typeof eventParticipationRequestNotificationGroupSchema + >; + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#GroupedNotificationsResults} + */ +const groupedNotificationsResultsSchema = v.object({ + accounts: filteredArray(accountSchema), + partial_accounts: v.fallback(v.optional(v.array(partialAccountWithAvatarSchema)), undefined), + statuses: filteredArray(statusSchema), + notification_groups: filteredArray(notificationGroupSchema), +}); + +/** + * @category Entity types + */ +type GroupedNotificationsResults = v.InferOutput; + +export { notificationGroupSchema, groupedNotificationsResultsSchema, type NotificationGroup, type GroupedNotificationsResults }; diff --git a/packages/pl-api/lib/entities/index.ts b/packages/pl-api/lib/entities/index.ts index 1411ead7f..5fb6d6339 100644 --- a/packages/pl-api/lib/entities/index.ts +++ b/packages/pl-api/lib/entities/index.ts @@ -41,6 +41,7 @@ export * from './filter'; export * from './group'; export * from './group-member'; export * from './group-relationship'; +export * from './grouped-notifications-results'; export * from './instance'; export * from './interaction-policy'; export * from './interaction-request'; diff --git a/packages/pl-api/lib/entities/instance.ts b/packages/pl-api/lib/entities/instance.ts index 22a7fc055..af3596a54 100644 --- a/packages/pl-api/lib/entities/instance.ts +++ b/packages/pl-api/lib/entities/instance.ts @@ -5,7 +5,7 @@ import { accountSchema } from './account'; import { ruleSchema } from './rule'; import { coerceObject, filteredArray, mimeSchema } from './utils'; -const getApiVersions = (instance: any) => ({ +const getApiVersions = (instance: any): Record => ({ ...Object.fromEntries(instance.pleroma?.metadata?.features?.map((feature: string) => { let string = `${feature}.pleroma.pl-api`; if (string.startsWith('pleroma:') || string.startsWith('pleroma_')) string = string.slice(8); @@ -166,7 +166,6 @@ const pleromaSchema = coerceObject({ birthday_min_age: v.fallback(v.number(), 0), birthday_required: v.fallback(v.boolean(), false), description_limit: v.fallback(v.number(), 1500), - features: v.fallback(v.array(v.string()), []), federation: coerceObject({ enabled: v.fallback(v.boolean(), true), // Assume true unless explicitly false mrf_policies: v.fallback(v.optional(v.array(v.string())), undefined), @@ -271,7 +270,6 @@ const instanceV1Schema = coerceObject({ description_limit: v.fallback(v.number(), 1500), email: v.fallback(v.pipe(v.string(), v.email()), ''), feature_quote: v.fallback(v.boolean(), false), - fedibird_capabilities: v.fallback(v.array(v.string()), []), languages: v.fallback(v.array(v.string()), []), max_media_attachments: v.fallback(v.optional(v.number()), undefined), max_toot_chars: v.fallback(v.optional(v.number()), undefined), @@ -292,7 +290,10 @@ const instanceV1Schema = coerceObject({ version: v.fallback(v.string(), '0.0.0'), }); -/** @see {@link https://docs.joinmastodon.org/entities/Instance/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Instance/} + */ const instanceSchema = v.pipe( v.any(), v.transform((data: any) => { @@ -305,7 +306,7 @@ const instanceSchema = v.pipe( if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions }; - return instanceV1ToV2({ ...data, api_versions: apiVersions }); + return { ...instanceV1ToV2(data), api_versions: apiVersions }; }), coerceObject({ account_domain: v.fallback(v.string(), ''), @@ -315,7 +316,10 @@ const instanceSchema = v.pipe( description: v.fallback(v.string(), ''), domain: v.fallback(v.string(), ''), feature_quote: v.fallback(v.boolean(), false), - fedibird_capabilities: v.fallback(v.array(v.string()), []), + icons: filteredArray(v.object({ + size: v.pipe(v.string(), v.regex(/^[0-9]+x[0-9]+$/)), + src: v.string(), + })), languages: v.fallback(v.array(v.string()), []), pleroma: pleromaSchema, registrations: registrations, @@ -328,6 +332,9 @@ const instanceSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Instance = v.InferOutput; export { instanceSchema, type Instance }; diff --git a/packages/pl-api/lib/entities/interaction-policy.ts b/packages/pl-api/lib/entities/interaction-policy.ts index ace8b2fba..36df12137 100644 --- a/packages/pl-api/lib/entities/interaction-policy.ts +++ b/packages/pl-api/lib/entities/interaction-policy.ts @@ -4,6 +4,9 @@ import { coerceObject } from './utils'; const interactionPolicyEntrySchema = v.picklist(['public', 'followers', 'following', 'mutuals', 'mentioned', 'author', 'me']); +/** + * @category Entity types + */ type InteractionPolicyEntry = v.InferOutput; const interactionPolicyRuleSchema = coerceObject({ @@ -11,15 +14,24 @@ const interactionPolicyRuleSchema = coerceObject({ with_approval: v.fallback(v.array(interactionPolicyEntrySchema), []), }); -/** @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} */ +/** + * @category Schemas + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ const interactionPolicySchema = coerceObject({ can_favourite: interactionPolicyRuleSchema, can_reblog: interactionPolicyRuleSchema, can_reply: interactionPolicyRuleSchema, }); +/** + * @category Entity types + */ type InteractionPolicy = v.InferOutput; +/** + * @category Schemas + */ const interactionPoliciesSchema = coerceObject({ public: interactionPolicySchema, unlisted: interactionPolicySchema, @@ -27,6 +39,9 @@ const interactionPoliciesSchema = coerceObject({ direct: interactionPolicySchema, }); +/** + * @category Entity types + */ type InteractionPolicies = v.InferOutput; export { interactionPolicySchema, interactionPoliciesSchema, type InteractionPolicyEntry, type InteractionPolicy, type InteractionPolicies }; diff --git a/packages/pl-api/lib/entities/interaction-request.ts b/packages/pl-api/lib/entities/interaction-request.ts index b917292ea..1bbd49bcf 100644 --- a/packages/pl-api/lib/entities/interaction-request.ts +++ b/packages/pl-api/lib/entities/interaction-request.ts @@ -4,7 +4,10 @@ import { accountSchema } from './account'; import { statusSchema } from './status'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.gotosocial.org/en/latest/api/swagger.yaml#/definitions/interactionRequest} */ +/** + * @category Schemas + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger.yaml#/definitions/interactionRequest} + */ const interactionRequestSchema = v.object({ accepted_at: v.fallback(v.nullable(datetimeSchema), null), account: accountSchema, @@ -17,6 +20,9 @@ const interactionRequestSchema = v.object({ uri: v.fallback(v.nullable(v.string()), null), }); +/** + * @category Entity types + */ type InteractionRequest = v.InferOutput; export { interactionRequestSchema, type InteractionRequest }; diff --git a/packages/pl-api/lib/entities/list.ts b/packages/pl-api/lib/entities/list.ts index 2ffc350a4..12278a7f3 100644 --- a/packages/pl-api/lib/entities/list.ts +++ b/packages/pl-api/lib/entities/list.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/List/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/List/} + */ const listSchema = v.object({ id: v.pipe(v.unknown(), v.transform(String)), title: v.string(), @@ -8,6 +11,9 @@ const listSchema = v.object({ exclusive: v.fallback(v.optional(v.boolean()), undefined), }); +/** + * @category Entity types + */ type List = v.InferOutput; export { listSchema, type List }; diff --git a/packages/pl-api/lib/entities/location.ts b/packages/pl-api/lib/entities/location.ts index ff723fa95..731e95ec9 100644 --- a/packages/pl-api/lib/entities/location.ts +++ b/packages/pl-api/lib/entities/location.ts @@ -1,5 +1,8 @@ import * as v from 'valibot'; +/** + * @category Schemas + */ const locationSchema = v.object({ url: v.fallback(v.pipe(v.string(), v.url()), ''), description: v.fallback(v.string(), ''), @@ -18,6 +21,9 @@ const locationSchema = v.object({ })), null), }); +/** + * @category Entity types + */ type Location = v.InferOutput; export { locationSchema, type Location }; diff --git a/packages/pl-api/lib/entities/marker.ts b/packages/pl-api/lib/entities/marker.ts index dd7a9483f..a0b370f21 100644 --- a/packages/pl-api/lib/entities/marker.ts +++ b/packages/pl-api/lib/entities/marker.ts @@ -2,6 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from './utils'; +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Marker/} + */ const markerSchema = v.pipe( v.any(), v.transform((marker: any) => marker ? ({ @@ -16,11 +20,19 @@ const markerSchema = v.pipe( }), ); -/** @see {@link https://docs.joinmastodon.org/entities/Marker/} */ +/** + * @category Entity types + */ type Marker = v.InferOutput; +/** + * @category Schemas + */ const markersSchema = v.record(v.string(), markerSchema); +/** + * @category Entity types + */ type Markers = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/media-attachment.ts b/packages/pl-api/lib/entities/media-attachment.ts index 47e9ee8f9..95b6c2451 100644 --- a/packages/pl-api/lib/entities/media-attachment.ts +++ b/packages/pl-api/lib/entities/media-attachment.ts @@ -3,6 +3,9 @@ import * as v from 'valibot'; import { mimeSchema } from './utils'; +/** + * @category Schemas + */ const blurhashSchema = v.pipe(v.string(), v.check( (value) => isBlurhashValid(value).result, 'invalid blurhash', // .errorReason @@ -87,7 +90,10 @@ const unknownAttachmentSchema = v.object({ type: v.literal('unknown'), }); -/** @see {@link https://docs.joinmastodon.org/entities/MediaAttachment} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/MediaAttachment} + */ const mediaAttachmentSchema = v.pipe( v.any(), v.transform((data: any) => { @@ -108,6 +114,9 @@ const mediaAttachmentSchema = v.pipe( ]), ); +/** + * @category Entity types + */ type MediaAttachment = v.InferOutput; export { blurhashSchema, mediaAttachmentSchema, type MediaAttachment }; diff --git a/packages/pl-api/lib/entities/mention.ts b/packages/pl-api/lib/entities/mention.ts index 43b15c25a..9671356cf 100644 --- a/packages/pl-api/lib/entities/mention.ts +++ b/packages/pl-api/lib/entities/mention.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Status/#Mention} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Status/#Mention} + */ const mentionSchema = v.pipe( v.object({ id: v.string(), @@ -17,6 +20,9 @@ const mentionSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Mention = v.InferOutput; export { mentionSchema, type Mention }; diff --git a/packages/pl-api/lib/entities/notification-policy.ts b/packages/pl-api/lib/entities/notification-policy.ts index ed964d15d..1adef7724 100644 --- a/packages/pl-api/lib/entities/notification-policy.ts +++ b/packages/pl-api/lib/entities/notification-policy.ts @@ -1,17 +1,26 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/NotificationPolicy} */ +const notificationPolicyRuleSchema = v.picklist(['accept', 'filter', 'drop']); + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/NotificationPolicy} + */ const notificationPolicySchema = v.object({ - filter_not_following: v.boolean(), - filter_not_followers: v.boolean(), - filter_new_accounts: v.boolean(), - filter_private_mentions: v.boolean(), + for_not_following: notificationPolicyRuleSchema, + for_not_followers: notificationPolicyRuleSchema, + for_new_accounts: notificationPolicyRuleSchema, + for_private_mentions: notificationPolicyRuleSchema, + for_limited_accounts: notificationPolicyRuleSchema, summary: v.object({ pending_requests_count: v.pipe(v.number(), v.integer()), pending_notifications_count: v.pipe(v.number(), v.integer()), }), }); +/** + * @category Entity types + */ type NotificationPolicy = v.InferOutput; export { notificationPolicySchema, type NotificationPolicy }; diff --git a/packages/pl-api/lib/entities/notification-request.ts b/packages/pl-api/lib/entities/notification-request.ts index cdef784d2..8c1fb8b7a 100644 --- a/packages/pl-api/lib/entities/notification-request.ts +++ b/packages/pl-api/lib/entities/notification-request.ts @@ -4,7 +4,10 @@ import { accountSchema } from './account'; import { statusSchema } from './status'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/NotificationRequest} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/NotificationRequest} + */ const notificationRequestSchema = v.object({ id: v.string(), created_at: datetimeSchema, @@ -14,6 +17,9 @@ const notificationRequestSchema = v.object({ last_status: v.fallback(v.optional(statusSchema), undefined), }); +/** + * @category Entity types + */ type NotificationRequest = v.InferOutput; export { notificationRequestSchema, type NotificationRequest }; diff --git a/packages/pl-api/lib/entities/notification.ts b/packages/pl-api/lib/entities/notification.ts index a22345711..388f2f424 100644 --- a/packages/pl-api/lib/entities/notification.ts +++ b/packages/pl-api/lib/entities/notification.ts @@ -83,7 +83,10 @@ const eventParticipationRequestNotificationSchema = v.object({ participation_message: v.fallback(v.nullable(v.string()), null), }); -/** @see {@link https://docs.joinmastodon.org/entities/Notification/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Notification/} + * */ const notificationSchema: v.BaseSchema> = v.pipe( v.any(), v.transform((notification: any) => ({ @@ -107,6 +110,9 @@ const notificationSchema: v.BaseSchema> eventParticipationRequestNotificationSchema, ])) as any; +/** + * @category Entity types + */ type Notification = v.InferOutput< | typeof accountNotificationSchema | typeof mentionNotificationSchema diff --git a/packages/pl-api/lib/entities/oauth-token.ts b/packages/pl-api/lib/entities/oauth-token.ts index f041ade19..808136fc5 100644 --- a/packages/pl-api/lib/entities/oauth-token.ts +++ b/packages/pl-api/lib/entities/oauth-token.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} */ +/** + * @category Schemas + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} + */ const oauthTokenSchema = v.pipe( v.any(), v.transform((token: any) => ({ @@ -16,6 +19,9 @@ const oauthTokenSchema = v.pipe( }), ); +/** + * @category Entity types + */ type OauthToken = v.InferOutput; export { oauthTokenSchema, type OauthToken }; diff --git a/packages/pl-api/lib/entities/poll.ts b/packages/pl-api/lib/entities/poll.ts index 24ebe6f9c..29bc9009f 100644 --- a/packages/pl-api/lib/entities/poll.ts +++ b/packages/pl-api/lib/entities/poll.ts @@ -10,7 +10,10 @@ const pollOptionSchema = v.object({ title_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null), }); -/** @see {@link https://docs.joinmastodon.org/entities/Poll/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Poll/} + */ const pollSchema = v.object({ emojis: filteredArray(customEmojiSchema), expired: v.fallback(v.boolean(), false), @@ -26,7 +29,14 @@ const pollSchema = v.object({ non_anonymous: v.fallback(v.boolean(), false), }); +/** + * @category Entity types + */ type Poll = v.InferOutput; + +/** + * @category Entity types + */ type PollOption = Poll['options'][number]; export { pollSchema, type Poll, type PollOption }; diff --git a/packages/pl-api/lib/entities/preview-card-author.ts b/packages/pl-api/lib/entities/preview-card-author.ts new file mode 100644 index 000000000..64faed7dc --- /dev/null +++ b/packages/pl-api/lib/entities/preview-card-author.ts @@ -0,0 +1,20 @@ +import * as v from 'valibot'; + +import { accountSchema } from './account'; + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/PreviewCardAuthor/} + */ +const previewCardAuthorSchema = v.object({ + name: v.string(), + url: v.pipe(v.string(), v.url()), + account: v.fallback(v.nullable(accountSchema), null), +}); + +/** + * @category Entity types + */ +type PreviewCardAuthor = v.InferOutput; + +export { previewCardAuthorSchema, type PreviewCardAuthor }; diff --git a/packages/pl-api/lib/entities/preview-card.ts b/packages/pl-api/lib/entities/preview-card.ts index 04c4cdc9e..e8a8710f8 100644 --- a/packages/pl-api/lib/entities/preview-card.ts +++ b/packages/pl-api/lib/entities/preview-card.ts @@ -1,9 +1,18 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/} */ +import { previewCardAuthorSchema } from './preview-card-author'; +import { filteredArray } from './utils'; + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/PreviewCard/} + */ const previewCardSchema = v.object({ + /** @deprecated */ author_name: v.fallback(v.string(), ''), + /** @deprecated */ author_url: v.fallback(v.pipe(v.string(), v.url()), ''), + authors: filteredArray(previewCardAuthorSchema), blurhash: v.fallback(v.nullable(v.string()), null), description: v.fallback(v.string(), ''), embed_url: v.fallback(v.pipe(v.string(), v.url()), ''), @@ -19,6 +28,9 @@ const previewCardSchema = v.object({ width: v.fallback(v.number(), 0), }); +/** + * @category Entity types + */ type PreviewCard = v.InferOutput; export { previewCardSchema, type PreviewCard }; diff --git a/packages/pl-api/lib/entities/relationship-severance-event.ts b/packages/pl-api/lib/entities/relationship-severance-event.ts index df76cfd0d..1d19a85ff 100644 --- a/packages/pl-api/lib/entities/relationship-severance-event.ts +++ b/packages/pl-api/lib/entities/relationship-severance-event.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/RelationshipSeveranceEvent/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/RelationshipSeveranceEvent/} + */ const relationshipSeveranceEventSchema = v.object({ id: v.string(), type: v.picklist(['domain_block', 'user_domain_block', 'account_suspension']), @@ -11,6 +14,9 @@ const relationshipSeveranceEventSchema = v.object({ created_at: datetimeSchema, }); +/** + * @category Entity types + */ type RelationshipSeveranceEvent = v.InferOutput; export { relationshipSeveranceEventSchema, type RelationshipSeveranceEvent }; diff --git a/packages/pl-api/lib/entities/relationship.ts b/packages/pl-api/lib/entities/relationship.ts index e2fc728af..31291f424 100644 --- a/packages/pl-api/lib/entities/relationship.ts +++ b/packages/pl-api/lib/entities/relationship.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Relationship/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Relationship/} + */ const relationshipSchema = v.object({ blocked_by: v.fallback(v.boolean(), false), blocking: v.fallback(v.boolean(), false), @@ -17,6 +20,9 @@ const relationshipSchema = v.object({ showing_reblogs: v.fallback(v.boolean(), false), }); +/** + * @category Entity types + */ type Relationship = v.InferOutput; export { relationshipSchema, type Relationship }; diff --git a/packages/pl-api/lib/entities/report.ts b/packages/pl-api/lib/entities/report.ts index 83970f0dc..2226f40de 100644 --- a/packages/pl-api/lib/entities/report.ts +++ b/packages/pl-api/lib/entities/report.ts @@ -3,7 +3,10 @@ import * as v from 'valibot'; import { accountSchema } from './account'; import { datetimeSchema } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Report/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Report/} + */ const reportSchema = v.object({ id: v.string(), action_taken: v.fallback(v.optional(v.boolean()), undefined), @@ -17,6 +20,9 @@ const reportSchema = v.object({ target_account: v.fallback(v.nullable(accountSchema), null), }); +/** + * @category Entity types + */ type Report = v.InferOutput; export { reportSchema, type Report }; diff --git a/packages/pl-api/lib/entities/role.ts b/packages/pl-api/lib/entities/role.ts index 1b1c2eab9..14584738e 100644 --- a/packages/pl-api/lib/entities/role.ts +++ b/packages/pl-api/lib/entities/role.ts @@ -2,6 +2,9 @@ import * as v from 'valibot'; const hexSchema = v.pipe(v.string(), v.regex(/^#[a-f0-9]{6}$/i)); +/** + * @category Schemas + */ const roleSchema = v.object({ id: v.fallback(v.string(), ''), name: v.fallback(v.string(), ''), @@ -10,6 +13,9 @@ const roleSchema = v.object({ highlighted: v.fallback(v.boolean(), true), }); +/** + * @category Entity types + */ type Role = v.InferOutput; export { diff --git a/packages/pl-api/lib/entities/rule.ts b/packages/pl-api/lib/entities/rule.ts index c1077cb0d..1bc5e6b40 100644 --- a/packages/pl-api/lib/entities/rule.ts +++ b/packages/pl-api/lib/entities/rule.ts @@ -6,7 +6,10 @@ const baseRuleSchema = v.object({ hint: v.fallback(v.string(), ''), }); -/** @see {@link https://docs.joinmastodon.org/entities/Rule/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Rule/} + */ const ruleSchema = v.pipe( v.any(), v.transform((data: any) => ({ @@ -16,6 +19,9 @@ const ruleSchema = v.pipe( baseRuleSchema, ); +/** + * @category Entity types + */ type Rule = v.InferOutput; export { ruleSchema, type Rule }; diff --git a/packages/pl-api/lib/entities/scheduled-status.ts b/packages/pl-api/lib/entities/scheduled-status.ts index 347cc0946..17bb70716 100644 --- a/packages/pl-api/lib/entities/scheduled-status.ts +++ b/packages/pl-api/lib/entities/scheduled-status.ts @@ -3,7 +3,10 @@ import * as v from 'valibot'; import { mediaAttachmentSchema } from './media-attachment'; import { datetimeSchema, filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/ScheduledStatus/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/ScheduledStatus/} + */ const scheduledStatusSchema = v.object({ id: v.string(), scheduled_at: datetimeSchema, @@ -31,6 +34,9 @@ const scheduledStatusSchema = v.object({ media_attachments: filteredArray(mediaAttachmentSchema), }); +/** + * @category Entity types + */ type ScheduledStatus = v.InferOutput; export { scheduledStatusSchema, type ScheduledStatus }; diff --git a/packages/pl-api/lib/entities/scrobble.ts b/packages/pl-api/lib/entities/scrobble.ts index 483805caf..b1b2d50ac 100644 --- a/packages/pl-api/lib/entities/scrobble.ts +++ b/packages/pl-api/lib/entities/scrobble.ts @@ -3,6 +3,9 @@ import * as v from 'valibot'; import { accountSchema } from './account'; import { datetimeSchema } from './utils'; +/** + * @category Schemas + */ const scrobbleSchema = v.pipe( v.any(), v.transform((scrobble: any) => scrobble ? { @@ -21,6 +24,9 @@ const scrobbleSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Scrobble = v.InferOutput; export { scrobbleSchema, type Scrobble }; diff --git a/packages/pl-api/lib/entities/search.ts b/packages/pl-api/lib/entities/search.ts index 250644d10..9af61536d 100644 --- a/packages/pl-api/lib/entities/search.ts +++ b/packages/pl-api/lib/entities/search.ts @@ -6,7 +6,10 @@ import { statusSchema } from './status'; import { tagSchema } from './tag'; import { filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/Search} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Search} + */ const searchSchema = v.object({ accounts: filteredArray(accountSchema), statuses: filteredArray(statusSchema), @@ -14,6 +17,9 @@ const searchSchema = v.object({ groups: filteredArray(groupSchema), }); +/** + * @category Entity types + */ type Search = v.InferOutput; export { searchSchema, type Search }; diff --git a/packages/pl-api/lib/entities/status-edit.ts b/packages/pl-api/lib/entities/status-edit.ts index 9aaa1a74c..46a3c159c 100644 --- a/packages/pl-api/lib/entities/status-edit.ts +++ b/packages/pl-api/lib/entities/status-edit.ts @@ -5,7 +5,10 @@ import { customEmojiSchema } from './custom-emoji'; import { mediaAttachmentSchema } from './media-attachment'; import { datetimeSchema, filteredArray } from './utils'; -/** @see {@link https://docs.joinmastodon.org/entities/StatusEdit/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/StatusEdit/} + */ const statusEditSchema = v.object({ content: v.fallback(v.string(), ''), spoiler_text: v.fallback(v.string(), ''), @@ -21,6 +24,9 @@ const statusEditSchema = v.object({ emojis: filteredArray(customEmojiSchema), }); +/** + * @category Entity types + */ type StatusEdit = v.InferOutput; export { statusEditSchema, type StatusEdit }; diff --git a/packages/pl-api/lib/entities/status-source.ts b/packages/pl-api/lib/entities/status-source.ts index 7b521cfa9..355c6a1c1 100644 --- a/packages/pl-api/lib/entities/status-source.ts +++ b/packages/pl-api/lib/entities/status-source.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { locationSchema } from './location'; -/** @see {@link https://docs.joinmastodon.org/entities/StatusSource/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/StatusSource/} + */ const statusSourceSchema = v.object({ id: v.string(), text: v.fallback(v.string(), ''), @@ -15,6 +18,9 @@ const statusSourceSchema = v.object({ spoiler_text_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null), }); +/** + * @category Entity types + */ type StatusSource = v.InferOutput; export { statusSourceSchema, type StatusSource }; diff --git a/packages/pl-api/lib/entities/status.ts b/packages/pl-api/lib/entities/status.ts index dd7b24f4d..6534e025b 100644 --- a/packages/pl-api/lib/entities/status.ts +++ b/packages/pl-api/lib/entities/status.ts @@ -134,6 +134,9 @@ const preprocess = (status: any) => { return status; }; +/** + * @category Schemas + */ const statusSchema: v.BaseSchema> = v.pipe(v.any(), v.transform(preprocess), v.object({ ...baseStatusSchema.entries, reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), @@ -141,6 +144,9 @@ const statusSchema: v.BaseSchema> = v.pipe(v.a quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), })) as any; +/** + * @category Schemas + */ const statusWithoutAccountSchema = v.pipe(v.any(), v.transform(preprocess), v.object({ ...(v.omit(baseStatusSchema, ['account']).entries), account: v.fallback(v.nullable(accountSchema), null), @@ -149,12 +155,18 @@ const statusWithoutAccountSchema = v.pipe(v.any(), v.transform(preprocess), v.ob quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null), })); +/** + * @category Entity types + */ type StatusWithoutAccount = Omit, 'account'> & { account: Account | null; reblog: Status | null; quote: Status | null; } +/** + * @category Entity types + */ type Status = v.InferOutput & { reblog: Status | null; quote: Status | null; diff --git a/packages/pl-api/lib/entities/streaming-event.ts b/packages/pl-api/lib/entities/streaming-event.ts index d41e6586f..d65ada9f3 100644 --- a/packages/pl-api/lib/entities/streaming-event.ts +++ b/packages/pl-api/lib/entities/streaming-event.ts @@ -8,6 +8,9 @@ import { markersSchema } from './marker'; import { notificationSchema } from './notification'; import { statusSchema } from './status'; +/** + * @category Schemas + */ const followRelationshipUpdateSchema = v.object({ state: v.picklist(['follow_pending', 'follow_accept', 'follow_reject']), follower: v.object({ @@ -22,6 +25,9 @@ const followRelationshipUpdateSchema = v.object({ }), }); +/** + * @category Entity types + */ type FollowRelationshipUpdate = v.InferOutput; const baseStreamingEventSchema = v.object({ @@ -96,7 +102,15 @@ const markerStreamingEventSchema = v.object({ payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), markersSchema), }); -/** @see {@link https://docs.joinmastodon.org/methods/streaming/#events} */ +const notificationsMergedEventSchema = v.object({ + ...baseStreamingEventSchema.entries, + event: v.literal('notifications_merged'), +}); + +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/methods/streaming/#events} + */ const streamingEventSchema: v.BaseSchema> = v.pipe( v.any(), v.transform((event: any) => ({ @@ -115,9 +129,13 @@ const streamingEventSchema: v.BaseSchema; export { diff --git a/packages/pl-api/lib/entities/suggestion.ts b/packages/pl-api/lib/entities/suggestion.ts index 875c0f8d1..897f42cf2 100644 --- a/packages/pl-api/lib/entities/suggestion.ts +++ b/packages/pl-api/lib/entities/suggestion.ts @@ -2,7 +2,10 @@ import * as v from 'valibot'; import { accountSchema } from './account'; -/** @see {@link https://docs.joinmastodon.org/entities/Suggestion} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Suggestion} + */ const suggestionSchema = v.pipe( v.any(), v.transform((suggestion: any) => { @@ -39,6 +42,9 @@ const suggestionSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Suggestion = v.InferOutput; export { suggestionSchema, type Suggestion }; diff --git a/packages/pl-api/lib/entities/tag.ts b/packages/pl-api/lib/entities/tag.ts index f9010d436..ae3aba9c8 100644 --- a/packages/pl-api/lib/entities/tag.ts +++ b/packages/pl-api/lib/entities/tag.ts @@ -1,12 +1,18 @@ import * as v from 'valibot'; +/** + * @category Schemas + */ const historySchema = v.array(v.object({ day: v.pipe(v.unknown(), v.transform(Number)), accounts: v.pipe(v.unknown(), v.transform(Number)), uses: v.pipe(v.unknown(), v.transform(Number)), })); -/** @see {@link https://docs.joinmastodon.org/entities/tag} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/tag} + */ const tagSchema = v.object({ name: v.pipe(v.string(), v.minLength(1)), url: v.fallback(v.pipe(v.string(), v.url()), ''), @@ -14,6 +20,9 @@ const tagSchema = v.object({ following: v.fallback(v.optional(v.boolean()), undefined), }); +/** + * @category Entity types + */ type Tag = v.InferOutput; export { historySchema, tagSchema, type Tag }; diff --git a/packages/pl-api/lib/entities/token.ts b/packages/pl-api/lib/entities/token.ts index 3fff4e40b..c66ef53bb 100644 --- a/packages/pl-api/lib/entities/token.ts +++ b/packages/pl-api/lib/entities/token.ts @@ -1,18 +1,24 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/Token/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Token/} + */ const tokenSchema = v.object({ access_token: v.string(), token_type: v.string(), scope: v.string(), created_at: v.fallback(v.optional(v.number()), undefined), - id: v.fallback(v.optional(v.number()), undefined), + id: v.fallback(v.optional(v.pipe(v.unknown(), v.transform(String))), undefined), refresh_token: v.fallback(v.optional(v.string()), undefined), expires_in: v.fallback(v.optional(v.number()), undefined), me: v.fallback(v.optional(v.string()), undefined), }); +/** + * @category Entity types + */ type Token = v.InferOutput; export { tokenSchema, type Token }; diff --git a/packages/pl-api/lib/entities/translation.ts b/packages/pl-api/lib/entities/translation.ts index 4ecc043a6..69a70796e 100644 --- a/packages/pl-api/lib/entities/translation.ts +++ b/packages/pl-api/lib/entities/translation.ts @@ -14,7 +14,10 @@ const translationMediaAttachment = v.object({ description: v.fallback(v.string(), ''), }); -/** @see {@link https://docs.joinmastodon.org/entities/Translation/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/Translation/} + */ const translationSchema = v.pipe( v.any(), v.transform((translation: any) => { @@ -40,6 +43,9 @@ const translationSchema = v.pipe( }), ); +/** + * @category Entity types + */ type Translation = v.InferOutput; export { translationSchema, type Translation }; diff --git a/packages/pl-api/lib/entities/trends-link.ts b/packages/pl-api/lib/entities/trends-link.ts index e2909347a..a199b49fd 100644 --- a/packages/pl-api/lib/entities/trends-link.ts +++ b/packages/pl-api/lib/entities/trends-link.ts @@ -3,7 +3,10 @@ import * as v from 'valibot'; import { blurhashSchema } from './media-attachment'; import { historySchema } from './tag'; -/** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/#trends-link} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/PreviewCard/#trends-link} + */ const trendsLinkSchema = v.pipe( v.any(), v.transform((link: any) => ({ ...link, id: link.url })), @@ -28,6 +31,9 @@ const trendsLinkSchema = v.pipe( }), ); +/** + * @category Entity types + */ type TrendsLink = v.InferOutput; export { trendsLinkSchema, type TrendsLink }; diff --git a/packages/pl-api/lib/entities/web-push-subscription.ts b/packages/pl-api/lib/entities/web-push-subscription.ts index 6ed56360b..20455c29a 100644 --- a/packages/pl-api/lib/entities/web-push-subscription.ts +++ b/packages/pl-api/lib/entities/web-push-subscription.ts @@ -1,6 +1,9 @@ import * as v from 'valibot'; -/** @see {@link https://docs.joinmastodon.org/entities/WebPushSubscription/} */ +/** + * @category Schemas + * @see {@link https://docs.joinmastodon.org/entities/WebPushSubscription/} + */ const webPushSubscriptionSchema = v.object({ id: v.pipe(v.unknown(), v.transform(String)), endpoint: v.string(), @@ -8,6 +11,9 @@ const webPushSubscriptionSchema = v.object({ server_key: v.string(), }); +/** + * @category Entity types + */ type WebPushSubscription = v.InferOutput; export { webPushSubscriptionSchema, type WebPushSubscription }; diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index 31261f0cd..d261f3b18 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -9,104 +9,151 @@ const any = (arr: Array): boolean => arr.some(Boolean); /** * Ditto, a Nostr server with Mastodon API. + * + * @category Software * @see {@link https://gitlab.com/soapbox-pub/ditto} */ const DITTO = 'Ditto'; /** * Firefish, a fork of Misskey. Formerly known as Calckey. + * + * @category Software * @see {@link https://joinfirefish.org/} */ const FIREFISH = 'Firefish'; /** * Friendica, decentralized social platform implementing multiple federation protocols. + * + * @category Software * @see {@link https://friendi.ca/} */ const FRIENDICA = 'Friendica'; /** * GoToSocial, an ActivityPub server written in Golang. + * + * @category Software * @see {@link https://gotosocial.org/} */ const GOTOSOCIAL = 'GoToSocial'; /** * Iceshrimp, yet another Misskey fork. + * + * @category Software * @see {@link https://iceshrimp.dev/} */ const ICESHRIMP = 'Iceshrimp'; +/** + * Iceshrimp.NET, a decentralized and federated social networking service, powered by .NET. + * + * @category Software + * @see {@link https://iceshrimp.dev/} + */ +const ICESHRIMP_NET = 'Iceshrimp.NET'; + /** * Mastodon, the software upon which this is all based. + * + * @category Software * @see {@link https://joinmastodon.org/} */ const MASTODON = 'Mastodon'; /** * Mitra, a Rust backend with cryptocurrency integrations. + * + * @category Software * @see {@link https://codeberg.org/silverpill/mitra} */ const MITRA = 'Mitra'; /** * Pixelfed, a federated image sharing platform. + * + * @category Software * @see {@link https://pixelfed.org/} */ const PIXELFED = 'Pixelfed'; /** * Pleroma, a feature-rich alternative written in Elixir. + * + * @category Software * @see {@link https://pleroma.social/} */ const PLEROMA = 'Pleroma'; /** * Takahē, backend with support for serving multiple domains. + * + * @category Software * @see {@link https://jointakahe.org/} */ const TAKAHE = 'Takahe'; /** * Toki, a C# Fediverse server. + * + * @category Software * @see {@link https://github.com/purifetchi/Toki} */ const TOKI = 'Toki'; /** * Akkoma, a Pleroma fork. + * + * @category Software * @see {@link https://akkoma.dev/AkkomaGang/akkoma} */ const AKKOMA = 'akkoma'; /** * glitch-soc, fork of Mastodon with a number of experimental features. + * + * @category Software * @see {@link https://glitch-soc.github.io/docs/} */ const GLITCH = 'glitch'; /** * glitch-soc, fork of Mastodon that provides local posting and a wider range of content types. + * + * @category Software * @see {@link https://github.com/hometown-fork/hometown} */ const HOMETOWN = 'hometown'; /** * Pl, fork of Pleroma developed by pl-api author. + * + * @category Software * @see {@link https://github.com/mkljczk/pl} */ const PL = 'pl'; /** * Rebased, fork of Pleroma developed by Soapbox author. + * + * @category Software * @see {@link https://gitlab.com/soapbox-pub/rebased} */ const REBASED = 'soapbox'; -/** Backend name reserved only for tests. */ +/** + * Backend name reserved only for tests. + * + * @category Software + */ const UNRELEASED = 'unreleased'; -/** Parse features for the given instance */ +/** + * Parse features for the given instance + * @category Utils + */ const getFeatures = (instance: Instance) => { const v = parseVersion(instance.version || ''); const federation = !!instance.pleroma.metadata.federation.enabled; @@ -210,6 +257,7 @@ const getFeatures = (instance: Instance) => { announcements: any([ v.software === FIREFISH, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, v.software === TAKAHE && gte(v.version, '0.7.0'), @@ -236,6 +284,7 @@ const getFeatures = (instance: Instance) => { * see POST /api/v1/bite */ bites: any([ + v.software === ICESHRIMP_NET, v.software === TOKI, instance.api_versions['bites.pleroma.pl-api'] >= 1, ]), @@ -262,6 +311,7 @@ const getFeatures = (instance: Instance) => { v.software === FIREFISH, v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === FRIENDICA, v.software === MASTODON, v.software === MITRA && gte(v.version, '3.3.0'), @@ -278,6 +328,7 @@ const getFeatures = (instance: Instance) => { bots: any([ v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, ]), @@ -312,6 +363,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL && gte(v.version, '0.17.0'), v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PIXELFED, v.software === PLEROMA, @@ -392,6 +444,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === MITRA, v.software === PIXELFED, @@ -408,6 +461,7 @@ const getFeatures = (instance: Instance) => { v.software === FIREFISH, v.software === FRIENDICA && gte(v.version, '2022.12.0'), v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === MITRA, v.software === TAKAHE && gte(v.version, '0.8.0'), @@ -480,6 +534,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === TAKAHE && gte(v.version, '0.6.1'), v.software === TOKI, @@ -516,6 +571,7 @@ const getFeatures = (instance: Instance) => { */ filtersV2: any([ v.software === GOTOSOCIAL && gte(v.version, '0.16.0'), + v.software === ICESHRIMP_NET, v.software === MASTODON, ]), @@ -577,9 +633,19 @@ const getFeatures = (instance: Instance) => { */ frontendConfigurations: any([ v.software === DITTO, + v.software === ICESHRIMP_NET, v.software === PLEROMA, ]), + /** + * @see GET /api/v2/notifications/:group_key + * @see GET /api/v2/notifications/:group_key + * @see POST /api/v2/notifications/:group_key/dismiss + * @see GET /api/v2/notifications/:group_key/accounts + * @see GET /api/v2/notifications/unread_count + */ + groupedNotifications: instance.api_versions.mastodon >= 2, + /** * Groups. * @see POST /api/v1/groups @@ -606,7 +672,9 @@ const getFeatures = (instance: Instance) => { * @see POST /api/v1/admin/groups/:group_id/unsuspend * @see DELETE /api/v1/admin/groups/:group_id */ - groups: instance.api_versions['pleroma:groups.pleroma.pl-api'] >= 1, + groups: instance.api_versions['groups.pleroma.pl-api'] >= 1, + + groupsSlugs: instance.api_versions['groups.pleroma.pl-api'] >= 1, /** * Can hide follows/followers lists and counts. @@ -692,6 +760,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, ]), @@ -762,6 +831,7 @@ const getFeatures = (instance: Instance) => { mediaV2: any([ v.software === FIREFISH, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === MITRA, v.software === PLEROMA, @@ -792,6 +862,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL && gte(v.version, '0.16.0'), v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === MITRA, v.software === PIXELFED, @@ -807,6 +878,7 @@ const getFeatures = (instance: Instance) => { v.software === FIREFISH, v.software === GOTOSOCIAL && gte(v.version, '0.16.0'), v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, v.software === TAKAHE, @@ -833,6 +905,11 @@ const getFeatures = (instance: Instance) => { */ notificationsExcludeVisibilities: v.software === PLEROMA, + /** + * @see GET /api/v1/notifications/unread_count + */ + notificationsGetUnreadCount: instance.api_versions.mastodon >= 1, + /** * Allows specifying notification types to include, rather than to exclude. * @see GET /api/v1/notifications @@ -840,12 +917,25 @@ const getFeatures = (instance: Instance) => { notificationsIncludeTypes: any([ v.software === FIREFISH, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA && gte(v.version, '2.5.0'), v.software === TAKAHE && gte(v.version, '0.6.2'), v.software === GOTOSOCIAL, ]), + /** + * @see GET /api/v2/notifications/policy + * @see PATCH /api/v2/notifications/policy + */ + notificationsPolicy: instance.api_versions.mastodon >= 1, + + /** + * @see POST /api/v1/notifications/requests/accept + * @see POST /api/v1/notifications/requests/dismiss + */ + notificationsRequestsAcceptMultiple: instance.api_versions.mastodon >= 1, + pleromaAdminAccounts: v.software === PLEROMA, /** @@ -890,6 +980,7 @@ const getFeatures = (instance: Instance) => { polls: any([ v.software === FIREFISH, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === TAKAHE && gte(v.version, '0.8.0'), v.software === GOTOSOCIAL, @@ -936,12 +1027,14 @@ const getFeatures = (instance: Instance) => { * @see PATCH /api/v1/accounts/update_credentials */ profileFields: any([ + v.software === DITTO, + v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, + v.software === MITRA, v.software === PLEROMA, v.software === TAKAHE && gte(v.version, '0.7.0'), - v.software === MITRA, - v.software === GOTOSOCIAL, ]), /** @@ -961,6 +1054,7 @@ const getFeatures = (instance: Instance) => { v.software === FRIENDICA, v.software === GOTOSOCIAL, v.software === ICESHRIMP, + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, v.software === TAKAHE, @@ -972,6 +1066,7 @@ const getFeatures = (instance: Instance) => { * @see POST /api/v1/statuses */ quotePosts: any([ + v.software === ICESHRIMP_NET, v.software === FRIENDICA && gte(v.version, '2023.3.0'), v.software === PLEROMA && [REBASED, AKKOMA].includes(v.build!) && gte(v.version, '2.5.0'), instance.api_versions['quote_posting.pleroma.pl-api'] >= 1, @@ -983,6 +1078,7 @@ const getFeatures = (instance: Instance) => { * @see POST /api/v1/statuses/:id/reblog */ reblogVisibility: any([ + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA, ]), @@ -998,6 +1094,7 @@ const getFeatures = (instance: Instance) => { * @see POST /api/v1/accounts/:id/remove_from_followers */ removeFromFollowers: any([ + v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA && gte(v.version, '2.5.0'), v.software === PLEROMA && v.build === AKKOMA, @@ -1056,10 +1153,11 @@ const getFeatures = (instance: Instance) => { * @see POST /api/v2/search */ searchFromAccount: any([ + v.software === DITTO, + v.software === GOTOSOCIAL, v.software === ICESHRIMP, v.software === MASTODON, v.software === PLEROMA, - v.software === GOTOSOCIAL, ]), /** @@ -1216,7 +1314,7 @@ interface Backend { /** Get information about the software from its version string */ const parseVersion = (version: string): Backend => { - const regex = /^([\w+.-]*)(?: \(compatible; ([\w]*) (.*)\))?$/; + const regex = /^([\w+.-]*)(?: \(compatible; ([\w.]*) (.*)\))?$/; const match = regex.exec(version.replace('/', ' ')); const semverString = match && (match[3] || match[1]); @@ -1249,6 +1347,7 @@ export { FRIENDICA, GOTOSOCIAL, ICESHRIMP, + ICESHRIMP_NET, MASTODON, MITRA, PIXELFED, diff --git a/packages/pl-api/lib/params/accounts.ts b/packages/pl-api/lib/params/accounts.ts index 6c22ed3ea..c3c58c216 100644 --- a/packages/pl-api/lib/params/accounts.ts +++ b/packages/pl-api/lib/params/accounts.ts @@ -1,7 +1,13 @@ import type { LanguageParam, OnlyEventsParam, OnlyMediaParam, PaginationParams, WithMutedParam, WithRelationshipsParam } from './common'; +/** + * @category Request params + */ type GetAccountParams = WithMutedParam; +/** + * @category Request params + */ interface GetAccountStatusesParams extends PaginationParams, WithMutedParam, OnlyEventsParam, OnlyMediaParam, LanguageParam { /** Boolean. Filter out statuses in reply to a different account. */ exclude_replies?: boolean; @@ -11,9 +17,19 @@ interface GetAccountStatusesParams extends PaginationParams, WithMutedParam, Onl tagged?: string; } +/** + * @category Request params + */ type GetAccountFollowersParams = PaginationParams & WithRelationshipsParam; + +/** + * @category Request params + */ type GetAccountFollowingParams = PaginationParams & WithRelationshipsParam; +/** + * @category Request params + */ interface FollowAccountParams { /** Boolean. Receive this account’s reblogs in home timeline? Defaults to true. */ reblogs?: boolean; @@ -21,16 +37,22 @@ interface FollowAccountParams { notify?: boolean; /** * Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages. - * Requires `features.followAccountLanguages`. + * Requires features{@link Features['followAccountLanguages']}. */ languages?: string[]; } +/** + * @category Request params + */ interface GetRelationshipsParams { /** Boolean. Whether relationships should be returned for suspended users, defaults to false. */ with_suspended?: boolean; } +/** + * @category Request params + */ interface SearchAccountParams { /** Integer. Maximum number of results. Defaults to 40 accounts. Max 80 accounts. */ limit?: number; @@ -42,6 +64,9 @@ interface SearchAccountParams { following?: boolean; } +/** + * @category Request params + */ interface ReportAccountParams { status_ids?: string[]; comment?: string; @@ -50,11 +75,24 @@ interface ReportAccountParams { rule_ids?: string[]; } +/** + * @category Request params + */ type GetAccountEndorsementsParams = WithRelationshipsParam; + +/** + * @category Request params + */ type GetAccountFavouritesParams = PaginationParams; +/** + * @category Request params + */ type GetScrobblesParams = PaginationParams; +/** + * @category Request params + */ interface CreateScrobbleParams { /** the title of the media playing */ title: string; diff --git a/packages/pl-api/lib/params/admin.ts b/packages/pl-api/lib/params/admin.ts index 6e256e6f3..21fa51e5b 100644 --- a/packages/pl-api/lib/params/admin.ts +++ b/packages/pl-api/lib/params/admin.ts @@ -1,5 +1,8 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface AdminGetAccountsParams extends PaginationParams { /** String. Filter for `local` or `remote` accounts. */ origin?: 'local' | 'remote'; @@ -23,8 +26,14 @@ interface AdminGetAccountsParams extends PaginationParams { ip?: string; } +/** + * @category Request params + */ type AdminAccountAction = 'none' | 'sensitive' | 'disable' | 'silence' | 'suspend'; +/** + * @category Request params + */ interface AdminPerformAccountActionParams { /** String. The ID of an associated report that caused this action to be taken. */ report_id?: string; @@ -36,8 +45,14 @@ interface AdminPerformAccountActionParams { send_email_notification?: boolean; } +/** + * @category Request params + */ type AdminGetDomainBlocksParams = PaginationParams; +/** + * @category Request params + */ interface AdminCreateDomainBlockParams { /** String. Whether to apply a `silence`, `suspend`, or `noop` to the domain. Defaults to `silence` */ severity?: 'silence' | 'suspend' | 'noop'; @@ -53,8 +68,14 @@ interface AdminCreateDomainBlockParams { obfuscate?: boolean; } +/** + * @category Request params + */ type AdminUpdateDomainBlockParams = AdminCreateDomainBlockParams; +/** + * @category Request params + */ interface AdminGetReportsParams extends PaginationParams { /** Boolean. Filter for resolved reports? */ resolved?: boolean; @@ -64,6 +85,9 @@ interface AdminGetReportsParams extends PaginationParams { target_account_id?: string; } +/** + * @category Request params + */ interface AdminUpdateReportParams { /** String. Change the classification of the report to `spam`, `violation`, or `other`. */ category?: 'spam' | 'violation' | 'other'; @@ -71,6 +95,9 @@ interface AdminUpdateReportParams { rule_ids?: string[]; } +/** + * @category Request params + */ interface AdminGetStatusesParams { limit?: number; local_only?: boolean; @@ -78,15 +105,27 @@ interface AdminGetStatusesParams { with_private?: boolean; } +/** + * @category Request params + */ interface AdminUpdateStatusParams { sensitive?: boolean; visibility?: 'public' | 'private' | 'unlisted'; } +/** + * @category Request params + */ type AdminGetCanonicalEmailBlocks = PaginationParams; +/** + * @category Request params + */ type AdminDimensionKey = 'languages' | 'sources' | 'servers' | 'space_usage' | 'software_versions' | 'tag_servers' | 'tag_languages' | 'instance_accounts' | 'instance_languages'; +/** + * @category Request params + */ interface AdminGetDimensionsParams { /** String (ISO 8601 Datetime). The start date for the time period. If a time is provided, it will be ignored. */ start_at?: string; @@ -112,12 +151,24 @@ interface AdminGetDimensionsParams { }; } +/** + * @category Request params + */ type AdminGetDomainAllowsParams = PaginationParams; +/** + * @category Request params + */ type AdminGetEmailDomainBlocksParams = PaginationParams; +/** + * @category Request params + */ type AdminGetIpBlocksParams = PaginationParams; +/** + * @category Request params + */ interface AdminCreateIpBlockParams { /** String. The IP address and prefix to block. Defaults to 0.0.0.0/32 */ ip?: string; @@ -129,10 +180,19 @@ interface AdminCreateIpBlockParams { expires_in?: number; } +/** + * @category Request params + */ type AdminUpdateIpBlockParams = Partial; +/** + * @category Request params + */ type AdminMeasureKey = 'active_users' | 'new_users' | 'interactions' | 'opened_reports' | 'resolved_reports' | 'tag_accounts' | 'tag_uses' | 'tag_servers' | 'instance_accounts' | 'instance_media_attachments' | 'instance_reports' | 'instance_statuses' | 'instance_follows' | 'instance_followers'; +/** + * @category Request params + */ interface AdminGetMeasuresParams { tag_accounts?: { /** String. When `tag_accounts` is one of the requested keys, you must provide a tag ID to obtain the measure of how many accounts used that hashtag in at least one status within the given time period. */ @@ -172,11 +232,17 @@ interface AdminGetMeasuresParams { }; } +/** + * @category Request params + */ interface AdminGetAnnouncementsParams { offset?: number; limit?: number; } +/** + * @category Request params + */ interface AdminCreateAnnouncementParams { /** announcement content */ content: string; @@ -188,8 +254,14 @@ interface AdminCreateAnnouncementParams { all_day?: boolean; } +/** + * @category Request params + */ type AdminUpdateAnnouncementParams = Partial; +/** + * @category Request params + */ interface AdminCreateDomainParams { /** domain name */ domain: string; @@ -197,6 +269,9 @@ interface AdminCreateDomainParams { public?: boolean; } +/** + * @category Request params + */ interface AdminGetModerationLogParams extends Pick { /** page number */ page?: number; @@ -210,14 +285,23 @@ interface AdminGetModerationLogParams extends Pick { search?: string; } +/** + * @category Request params + */ interface AdminCreateRuleParams { text: string; hint?: string; priority?: number; } +/** + * @category Request params + */ type AdminUpdateRuleParams = Partial; +/** + * @category Request params + */ interface AdminGetGroupsParams { } diff --git a/packages/pl-api/lib/params/apps.ts b/packages/pl-api/lib/params/apps.ts index 538ac1a34..17e5a839d 100644 --- a/packages/pl-api/lib/params/apps.ts +++ b/packages/pl-api/lib/params/apps.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface CreateApplicationParams { /** String. A name for your application */ client_name: string; diff --git a/packages/pl-api/lib/params/chats.ts b/packages/pl-api/lib/params/chats.ts index bf19f88e0..c9e16a5db 100644 --- a/packages/pl-api/lib/params/chats.ts +++ b/packages/pl-api/lib/params/chats.ts @@ -1,8 +1,18 @@ import { PaginationParams, WithMutedParam } from './common'; +/** + * @category Request params + */ type GetChatsParams = PaginationParams & WithMutedParam; + +/** + * @category Request params + */ type GetChatMessagesParams = PaginationParams; +/** + * @category Request params + */ type CreateChatMessageParams = { content?: string; media_id: string; diff --git a/packages/pl-api/lib/params/common.ts b/packages/pl-api/lib/params/common.ts index ed239f0cb..ab62db18f 100644 --- a/packages/pl-api/lib/params/common.ts +++ b/packages/pl-api/lib/params/common.ts @@ -13,7 +13,7 @@ interface WithMutedParam { /** * Boolean. Also show statuses from muted users. Default to false. * - * Requires `features.timelinesWithMuted`. + * Requires features{@link Features['timelinesWithMuted']}. */ with_muted?: boolean; } @@ -35,7 +35,7 @@ interface OnlyEventsParam { /** * Boolean. Filter out statuses without events. * - * Requires `features.events`. + * Requires features{@link Features['events']}. */ only_events?: boolean; } @@ -44,7 +44,7 @@ interface LanguageParam { /** * Fetch a translation in given language * - * Requires `features.fetchStatusesWithTranslation`. + * Requires features{@link Features['fetchStatusesWithTranslation']}. */ language?: string; } diff --git a/packages/pl-api/lib/params/events.ts b/packages/pl-api/lib/params/events.ts index c4120017a..4b40ab030 100644 --- a/packages/pl-api/lib/params/events.ts +++ b/packages/pl-api/lib/params/events.ts @@ -1,5 +1,8 @@ import { PaginationParams } from './common'; +/** + * @category Request params + */ interface CreateEventParams { /** name of the event */ name: string; @@ -19,9 +22,24 @@ interface CreateEventParams { content_type?: string; } +/** + * @category Request params + */ type EditEventParams = Partial>; + +/** + * @category Request params + */ type GetJoinedEventsParams = PaginationParams; + +/** + * @category Request params + */ type GetEventParticipationsParams = PaginationParams; + +/** + * @category Request params + */ type GetEventParticipationRequestsParams = PaginationParams; export type { diff --git a/packages/pl-api/lib/params/filtering.ts b/packages/pl-api/lib/params/filtering.ts index 165dff6ac..3fa4e1bfa 100644 --- a/packages/pl-api/lib/params/filtering.ts +++ b/packages/pl-api/lib/params/filtering.ts @@ -1,5 +1,8 @@ import type { PaginationParams, WithRelationshipsParam } from './common'; +/** + * @category Request params + */ interface MuteAccountParams { /** Boolean. Mute notifications in addition to statuses? Defaults to true. */ notifications?: boolean; @@ -7,12 +10,29 @@ interface MuteAccountParams { duration?: number; } +/** + * @category Request params + */ type GetMutesParams = Omit & WithRelationshipsParam; + +/** + * @category Request params + */ type GetBlocksParams = PaginationParams & WithRelationshipsParam; + +/** + * @category Request params + */ type GetDomainBlocksParams = PaginationParams; +/** + * @category Request params + */ type FilterContext = 'home' | 'notifications' | 'public' | 'thread' | 'account'; +/** + * @category Request params + */ interface CreateFilterParams { title: string; context: Array; @@ -24,6 +44,9 @@ interface CreateFilterParams { }>; } +/** + * @category Request params + */ interface UpdateFilterParams { title?: string; context?: Array; diff --git a/packages/pl-api/lib/params/grouped-notifications.ts b/packages/pl-api/lib/params/grouped-notifications.ts new file mode 100644 index 000000000..a46ab2024 --- /dev/null +++ b/packages/pl-api/lib/params/grouped-notifications.ts @@ -0,0 +1,37 @@ +import type { PaginationParams } from './common'; + +/** + * @category Request params + */ +interface GetGroupedNotificationsParams extends PaginationParams { + /** Types to include in the result. */ + types?: Array; + /** Types to exclude from the results. */ + exclude_types?: Array; + /** Return only notifications received from the specified account. */ + acccount_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. */ + grouped_types?: Array; + /** Whether to include notifications filtered by the user’s NotificationPolicy. Defaults to false. */ + include_filtered?: boolean; +} + +/** + * @category Request params + */ +interface GetUnreadNotificationGroupCountParams { + /** Maximum number of results to return. Defaults to 100 notifications. Max 1000 notifications. */ + limit?: number; + /** Types of notifications that should count towards unread notifications. */ + types?: Array; + /** Types of notifications that should not count towards unread notifications. */ + exclude_types?: Array; + /** Only count unread notifications received from the specified account. */ + account_id?: string; + /** 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/unread_count instead. */ + grouped_types?: Array; +} + +export type { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams }; diff --git a/packages/pl-api/lib/params/groups.ts b/packages/pl-api/lib/params/groups.ts index 5ec218914..e6ad616bd 100644 --- a/packages/pl-api/lib/params/groups.ts +++ b/packages/pl-api/lib/params/groups.ts @@ -1,5 +1,8 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface CreateGroupParams { display_name: string; note?: string; @@ -7,6 +10,9 @@ interface CreateGroupParams { header?: File; } +/** + * @category Request params + */ interface UpdateGroupParams { display_name?: string; note?: string; @@ -14,8 +20,19 @@ interface UpdateGroupParams { header?: File | ''; } +/** + * @category Request params + */ type GetGroupMembershipsParams = Omit; + +/** + * @category Request params + */ type GetGroupMembershipRequestsParams = Omit; + +/** + * @category Request params + */ type GetGroupBlocksParams = Omit; export type { diff --git a/packages/pl-api/lib/params/index.ts b/packages/pl-api/lib/params/index.ts index dcf70254f..08b7056b0 100644 --- a/packages/pl-api/lib/params/index.ts +++ b/packages/pl-api/lib/params/index.ts @@ -4,6 +4,7 @@ export * from './apps'; export * from './chats'; export * from './events'; export * from './filtering'; +export * from './grouped-notifications'; export * from './groups'; export * from './instance'; export * from './interaction-requests'; diff --git a/packages/pl-api/lib/params/instance.ts b/packages/pl-api/lib/params/instance.ts index c8a9fa4fa..4d5652573 100644 --- a/packages/pl-api/lib/params/instance.ts +++ b/packages/pl-api/lib/params/instance.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface ProfileDirectoryParams { /** Number. Skip the first n results. */ offset?: number; diff --git a/packages/pl-api/lib/params/interaction-requests.ts b/packages/pl-api/lib/params/interaction-requests.ts index 403c05b1b..b5c53fac5 100644 --- a/packages/pl-api/lib/params/interaction-requests.ts +++ b/packages/pl-api/lib/params/interaction-requests.ts @@ -1,5 +1,8 @@ import { PaginationParams } from './common'; +/** + * @category Request params + */ interface GetInteractionRequestsParams extends PaginationParams { /** If set, then only interactions targeting the given status_id will be included in the results. */ status_id?: string; diff --git a/packages/pl-api/lib/params/lists.ts b/packages/pl-api/lib/params/lists.ts index af98e821f..3eec175fb 100644 --- a/packages/pl-api/lib/params/lists.ts +++ b/packages/pl-api/lib/params/lists.ts @@ -1,5 +1,8 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface CreateListParams { /** String. The title of the list to be created. */ title: string; @@ -9,7 +12,14 @@ interface CreateListParams { exclusive?: boolean; } +/** + * @category Request params + */ type UpdateListParams = CreateListParams; + +/** + * @category Request params + */ type GetListAccountsParams = PaginationParams; export type { diff --git a/packages/pl-api/lib/params/media.ts b/packages/pl-api/lib/params/media.ts index 288f5ba8c..973c0a13e 100644 --- a/packages/pl-api/lib/params/media.ts +++ b/packages/pl-api/lib/params/media.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface UploadMediaParams { /** Object. The file to be attached, encoded using multipart form data. The file must have a MIME type. */ file: File; @@ -12,6 +15,9 @@ interface UploadMediaParams { focus?: string; } +/** + * @category Request params + */ interface UpdateMediaParams { /** Object. The custom thumbnail of the media to be attached, encoded using multipart form data. */ thumbnail?: File; diff --git a/packages/pl-api/lib/params/my-account.ts b/packages/pl-api/lib/params/my-account.ts index d0c372e76..039132e9c 100644 --- a/packages/pl-api/lib/params/my-account.ts +++ b/packages/pl-api/lib/params/my-account.ts @@ -1,23 +1,47 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface GetBookmarksParams extends PaginationParams { /** * Bookmark folder ID - * Requires `features.bookmarkFolders`. + * Requires features{@link Features['bookmarkFolders']}. */ folder_id?: string; } +/** + * @category Request params + */ type GetFavouritesParams = PaginationParams; + +/** + * @category Request params + */ type GetFollowRequestsParams = Omit; + +/** + * @category Request params + */ type GetEndorsementsParams = Omit; + +/** + * @category Request params + */ type GetFollowedTagsParams = PaginationParams; +/** + * @category Request params + */ interface CreateBookmarkFolderParams { name: string; emoji?: string; } +/** + * @category Request params + */ type UpdateBookmarkFolderParams = Partial; export type { diff --git a/packages/pl-api/lib/params/notifications.ts b/packages/pl-api/lib/params/notifications.ts index 488dd807c..b970d2ccb 100644 --- a/packages/pl-api/lib/params/notifications.ts +++ b/packages/pl-api/lib/params/notifications.ts @@ -1,34 +1,65 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface GetNotificationParams extends PaginationParams { - /** Array of String. Types to include in the result. */ + /** Types to include in the result. */ types?: string[]; - /** Array of String. Types to exclude from the results. */ + /** Types to exclude from the results. */ exclude_types?: string[]; - /** String. Return only notifications received from the specified account. */ + /** Return only notifications received from the specified account. */ account_id?: string; + /** + * Whether to include notifications filtered by the user’s NotificationPolicy. Defaults to false. + * Requires features.{@link Features['notificationsPolicy']}. + */ + include_filtered?: boolean; /** * will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). - * Requires `features.notificationsExcludeVisibilities`. + * Requires features{@link Features['notificationsExcludeVisibilities']}. */ exclude_visibilities?: string[]; } -interface UpdateNotificationPolicyRequest { - /** Boolean. Whether to filter notifications from accounts the user is not following. */ - filter_not_following?: boolean; - /** Boolean. Whether to filter notifications from accounts that are not following the user. */ - filter_not_followers?: boolean; - /** Boolean. Whether to filter notifications from accounts created in the past 30 days. */ - filter_new_accounts?: boolean; - /** Boolean. Whether to filter notifications from private mentions. Replies to private mentions initiated by the user, as well as accounts the user follows, are never filtered. */ - filter_private_mentions?: boolean; +/** + * @category Request params + */ +interface GetUnreadNotificationCountParams { + /** Maximum number of results to return. Defaults to 100 notifications. Max 1000 notifications. */ + limit?: number; + /** Types of notifications that should count towards unread notifications. */ + types?: string[]; + /** Types of notifications that should not count towards unread notifications */ + exclude_types?: string[]; + /** Only count unread notifications received from the specified account. */ + account_id?: string; } +/** + * @category Request params + */ +interface UpdateNotificationPolicyRequest { + /** Whether to `accept`, `filter` or `drop` notifications from accounts the user is not following. */ + for_not_following?: boolean; + /** Whether to `accept`, `filter` or `drop` notifications from accounts that are not following the user. */ + for_not_followers?: boolean; + /** Whether to `accept`, `filter` or `drop` notifications from accounts created in the past 30 days. */ + for_new_accounts?: boolean; + /** Whether to `accept`, `filter` or `drop` notifications from private mentions. drop will prevent creation of the notification object altogether (without preventing the underlying activity), */ + for_private_mentions?: boolean; + /** Whether to `accept`, `filter` or `drop` notifications from accounts that were limited by a moderator. */ + for_limited_accounts?: boolean; +} + +/** + * @category Request params + */ type GetNotificationRequestsParams = PaginationParams; export type { GetNotificationParams, + GetUnreadNotificationCountParams, UpdateNotificationPolicyRequest, GetNotificationRequestsParams, }; diff --git a/packages/pl-api/lib/params/oauth.ts b/packages/pl-api/lib/params/oauth.ts index 75e6059b3..b912d936f 100644 --- a/packages/pl-api/lib/params/oauth.ts +++ b/packages/pl-api/lib/params/oauth.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface OauthAuthorizeParams { /** String. Should be set equal to `code`. */ response_type: string; @@ -13,6 +16,9 @@ interface OauthAuthorizeParams { lang?: string; } +/** + * @category Request params + */ interface GetTokenParams { /** String. Set equal to `authorization_code` if `code` is provided in order to gain user-level access. Otherwise, set equal to `client_credentials` to obtain app-level access only. */ grant_type: string; @@ -28,6 +34,9 @@ interface GetTokenParams { scope?: string; } +/** + * @category Request params + */ interface RevokeTokenParams { /** String. The client ID, obtained during app registration. */ client_id: string; @@ -37,6 +46,9 @@ interface RevokeTokenParams { token: string; } +/** + * @category Request params + */ interface MfaChallengeParams { client_id: string; client_secret: string; diff --git a/packages/pl-api/lib/params/push-notifications.ts b/packages/pl-api/lib/params/push-notifications.ts index 67b85dd91..5a561bd7a 100644 --- a/packages/pl-api/lib/params/push-notifications.ts +++ b/packages/pl-api/lib/params/push-notifications.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface CreatePushNotificationsSubscriptionParams { subscription: { /** String. The endpoint URL that is called when a notification event occurs. */ @@ -16,6 +19,9 @@ interface CreatePushNotificationsSubscriptionParams { }; } +/** + * @category Request params + */ interface UpdatePushNotificationsSubscriptionParams { data?: { alerts?: Record; diff --git a/packages/pl-api/lib/params/scheduled-statuses.ts b/packages/pl-api/lib/params/scheduled-statuses.ts index 53875e490..4fdb0fa5b 100644 --- a/packages/pl-api/lib/params/scheduled-statuses.ts +++ b/packages/pl-api/lib/params/scheduled-statuses.ts @@ -1,5 +1,8 @@ import type { PaginationParams } from './common'; +/** + * @category Request params + */ type GetScheduledStatusesParams = PaginationParams; export type { diff --git a/packages/pl-api/lib/params/search.ts b/packages/pl-api/lib/params/search.ts index ecbeb2e30..4f3df33ea 100644 --- a/packages/pl-api/lib/params/search.ts +++ b/packages/pl-api/lib/params/search.ts @@ -1,5 +1,8 @@ import type { PaginationParams, WithRelationshipsParam } from './common'; +/** + * @category Request params + */ interface SearchParams extends Exclude, WithRelationshipsParam { /** String. Specify whether to search for only `accounts`, `hashtags`, `statuses` */ type?: 'accounts' | 'hashtags' | 'statuses' | 'groups'; diff --git a/packages/pl-api/lib/params/settings.ts b/packages/pl-api/lib/params/settings.ts index 9dc5a3e9a..01e3f2a53 100644 --- a/packages/pl-api/lib/params/settings.ts +++ b/packages/pl-api/lib/params/settings.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface CreateAccountParams { /** String. The desired username for the account */ username: string; @@ -30,6 +33,9 @@ interface CreateAccountParams { accepts_email_list?: boolean; } +/** + * @category Request params + */ interface UpdateCredentialsParams { /** String. The display name to use for the profile. */ display_name?: string; @@ -100,21 +106,25 @@ interface UpdateCredentialsParams { /** * Description of avatar image, for alt-text. - * Requires `features.accountAvatarDescription`. + * + * Requires features{@link Features['accountAvatarDescription']}. */ avatar_description?: boolean; /** * Description of header image, for alt-text. - * Requires `features.accountAvatarDescription`. + * Requires features{@link Features['accountAvatarDescription']}. */ header_description?: boolean; /** * Enable RSS feed for this account's Public posts at `/[username]/feed.rss` - * Requires `features.accountEnableRss`. + * Requires features{@link Features['accountEnableRss']}. */ enable_rss?: boolean; } +/** + * @category Request params + */ interface UpdateNotificationSettingsParams { /** * blocks notifications from accounts you do not follow @@ -127,6 +137,9 @@ interface UpdateNotificationSettingsParams { hide_notification_contents?: boolean; } +/** + * @category Request params + */ type UpdateInteractionPoliciesParams = Record< 'public' | 'unlisted' | 'private' | 'direct', Record< diff --git a/packages/pl-api/lib/params/statuses.ts b/packages/pl-api/lib/params/statuses.ts index 3b8a5bd68..5e5bdb03f 100644 --- a/packages/pl-api/lib/params/statuses.ts +++ b/packages/pl-api/lib/params/statuses.ts @@ -1,5 +1,10 @@ +import { UpdateInteractionPoliciesParams } from './settings'; + import type { PaginationParams } from './common'; +/** + * @category Request params + */ interface CreateStatusWithContent { /** The text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided. */ status: string; @@ -7,6 +12,9 @@ interface CreateStatusWithContent { media_ids?: string[]; } +/** + * @category Request params + */ interface CreateStatusWithMedia { /** The text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided. */ status?: string; @@ -14,6 +22,9 @@ interface CreateStatusWithMedia { media_ids: string[]; } +/** + * @category Request params + */ interface CreateStatusOptionalParams { poll?: { /** Array of String. Possible answers to the poll. If provided, `media_ids` cannot be used, and poll[expires_in] must be provided. */ @@ -35,8 +46,8 @@ interface CreateStatusOptionalParams { spoiler_text?: string; /** * String. Sets the visibility of the posted status to `public`, `unlisted`, `private`, `direct`. - * `local` — requires `features.createStatusLocalScope`. - * `list:LIST_ID` — requires `features.createStatusListScope`. + * `local` — Requires features{@link Features['createStatusLocalScope']}. + * `list:LIST_ID` — Requires features{@link Features['createStatusListScope']}. */ visibility?: string; /** String. ISO 639 language code for this status. */ @@ -46,7 +57,7 @@ interface CreateStatusOptionalParams { /** * boolean, if set to true the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example. - * Requires `features.createStatusPreview`. + * Requires features{@link Features['createStatusPreview']}. */ preview?: boolean; /** @@ -55,22 +66,22 @@ interface CreateStatusOptionalParams { content_type?: string; /** * A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for post visibility are not affected by this and will still apply. - * Requires `features.createStatusExplicitAddressing`. + * Requires features{@link Features['createStatusExplicitAddressing']}. */ to?: string[]; /** * The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour. - * Requires `features.createStatusExpiration`. + * Requires features{@link Features['createStatusExpiration']}. */ expires_in?: number; /** * Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. - * Requires `features.createStatusReplyToConversation`. + * Requires features{@link Features['createStatusReplyToConversation']}. */ in_reply_to_conversation_id?: string; /** * ID of the status being quoted, if any. - * Requires `features.quotePosts`. + * Requires features{@link Features['quotePosts']}. */ quote_id?: string; @@ -83,32 +94,66 @@ interface CreateStatusOptionalParams { status_map?: Record; spoiler_text_map?: Record; + + /** The 'interaction_policy' field can be used to set an interaction policy for this status. */ + interaction_policy?: UpdateInteractionPoliciesParams['public']; } +/** + * @category Request params + */ type CreateStatusParams = (CreateStatusWithContent | CreateStatusWithMedia) & CreateStatusOptionalParams; +/** + * @category Request params + */ interface LanguageParam { - /** Attach translated version of a post. Requires `features.autoTranslate`. */ + /** Attach translated version of a post. Requires features{@link Features['autoTranslate']}. */ language?: string; } +/** + * @category Request params + */ type GetStatusParams = LanguageParam; +/** + * @category Request params + */ type GetStatusesParams = LanguageParam; +/** + * @category Request params + */ type GetStatusContextParams = LanguageParam; +/** + * @category Request params + */ type GetRebloggedByParams = Omit +/** + * @category Request params + */ type GetFavouritedByParams = Omit +/** + * @category Request params + */ interface EditStatusOptionalParams { sensitive?: boolean; spoiler_text?: string; language?: string; } +/** + * @category Request params + */ type EditStatusParams = (CreateStatusWithContent | CreateStatusWithMedia) & EditStatusOptionalParams; + +/** + * @category Request params + */ type GetStatusQuotesParams = PaginationParams; export type { diff --git a/packages/pl-api/lib/params/timelines.ts b/packages/pl-api/lib/params/timelines.ts index 9a0377981..34e880ca6 100644 --- a/packages/pl-api/lib/params/timelines.ts +++ b/packages/pl-api/lib/params/timelines.ts @@ -1,5 +1,8 @@ import type { LanguageParam, OnlyEventsParam, OnlyMediaParam, PaginationParams, WithMutedParam } from './common'; +/** + * @category Request params + */ interface PublicTimelineParams extends PaginationParams, WithMutedParam, OnlyEventsParam, OnlyMediaParam, LanguageParam { /** Boolean. Show only local statuses? Defaults to false. */ local?: boolean; @@ -8,11 +11,14 @@ interface PublicTimelineParams extends PaginationParams, WithMutedParam, OnlyEve /** * Boolean. Show only statuses from the given domain. * - * Requires `features.instanceTimeline`. + * Requires features{@link Features['instanceTimeline']}. */ instance?: string; } +/** + * @category Request params + */ interface HashtagTimelineParams extends PaginationParams, WithMutedParam, OnlyEventsParam, OnlyMediaParam, LanguageParam { /** Array of String. Return statuses that contain any of these additional tags. */ any?: string[]; @@ -26,18 +32,35 @@ interface HashtagTimelineParams extends PaginationParams, WithMutedParam, OnlyEv remote?: boolean; } +/** + * @category Request params + */ type HomeTimelineParams = PaginationParams & WithMutedParam & OnlyEventsParam & LanguageParam; + +/** + * @category Request params + */ type LinkTimelineParams = PaginationParams & WithMutedParam & LanguageParam; + +/** + * @category Request params + */ type ListTimelineParams = PaginationParams & WithMutedParam & OnlyEventsParam & LanguageParam; +/** + * @category Request params + */ interface GetConversationsParams extends PaginationParams, LanguageParam { /** * Only return conversations with the given recipients (a list of user ids). - * Requires `features.conversationsByRecipients`. + * Requires features{@link Features['conversationsByRecipients']}. * */ recipients?: string[]; } +/** + * @category Request params + */ interface SaveMarkersParams { home?: { /** String. ID of the last status read in the home timeline. */ @@ -49,7 +72,14 @@ interface SaveMarkersParams { }; } +/** + * @category Request params + */ type GroupTimelineParams = PaginationParams & WithMutedParam & OnlyMediaParam & LanguageParam; + +/** + * @category Request params + */ type BubbleTimelineParams = PaginationParams & WithMutedParam & OnlyEventsParam & OnlyMediaParam & LanguageParam; export type { diff --git a/packages/pl-api/lib/params/trends.ts b/packages/pl-api/lib/params/trends.ts index e167f3ca3..b4ff665c5 100644 --- a/packages/pl-api/lib/params/trends.ts +++ b/packages/pl-api/lib/params/trends.ts @@ -1,3 +1,6 @@ +/** + * @category Request params + */ interface GetTrends { /** Integer. Maximum number of results to return. */ limit?: number; @@ -5,8 +8,19 @@ interface GetTrends { offset?: number; } +/** + * @category Request params + */ type GetTrendingTags = GetTrends; + +/** + * @category Request params + */ type GetTrendingStatuses = GetTrends; + +/** + * @category Request params + */ type GetTrendingLinks = GetTrends; export type { diff --git a/packages/pl-api/lib/responses.ts b/packages/pl-api/lib/responses.ts index a50480071..fc5f528fa 100644 --- a/packages/pl-api/lib/responses.ts +++ b/packages/pl-api/lib/responses.ts @@ -1,7 +1,10 @@ -interface PaginatedResponse { - previous: (() => Promise>) | null; - next: (() => Promise>) | null; - items: Array; +/** + * @category Utils + */ +interface PaginatedResponse { + previous: (() => Promise>) | null; + next: (() => Promise>) | null; + items: IsArray extends true ? Array : T; partial: boolean; total?: number; } diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 3158d0da7..7816f8a90 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,8 +1,8 @@ { "name": "pl-api", - "version": "0.1.5", + "version": "1.0.0-rc.4", "type": "module", - "homepage": "https://github.com/mkljczk/pl-fe/tree/fork/packages/pl-api", + "homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api", "repository": { "type": "git", "url": "https://github.com/mkljczk/pl-fe" @@ -20,27 +20,34 @@ "devDependencies": { "@stylistic/eslint-plugin": "^2.8.0", "@types/http-link-header": "^1.0.7", + "@types/lodash.omit": "^4.5.9", "@types/lodash.pick": "^4.4.9", - "@types/node": "^22.7.4", + "@types/node": "^22.9.3", "@types/semver": "^7.5.8", - "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.8.0", "eslint": "8.57.1", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-compat": "^6.0.1", - "eslint-plugin-import": "^2.30.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-promise": "^6.0.0", + "typedoc": "^0.26.10", + "typedoc-material-theme": "^1.1.0", + "typedoc-plugin-valibot": "^1.0.0", "typescript": "^5.6.2", "vite": "^5.4.8", - "vite-plugin-dts": "^4.2.3" + "vite-plugin-dts": "^4.2.3", + "ws": "^8.18.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "dependencies": { "blurhash": "^2.0.5", "http-link-header": "^1.1.3", + "isows": "^1.0.6", + "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", "object-to-formdata": "^4.5.1", - "query-string": "^9.1.0", + "query-string": "^9.1.1", "semver": "^7.6.3", "valibot": "^0.42.1" }, diff --git a/packages/pl-api/typedoc.config.mjs b/packages/pl-api/typedoc.config.mjs new file mode 100644 index 000000000..ce91934d2 --- /dev/null +++ b/packages/pl-api/typedoc.config.mjs @@ -0,0 +1,12 @@ +/** @type {Partial} */ + +const config = { + entryPoints: ['./lib/main.ts'], + plugin: ['typedoc-material-theme', 'typedoc-plugin-valibot'], + themeColor: '#d80482', + navigation: { + includeCategories: true, + }, +}; + +export default config; diff --git a/packages/pl-api/yarn.lock b/packages/pl-api/yarn.lock index ac923967f..5c0644a71 100644 --- a/packages/pl-api/yarn.lock +++ b/packages/pl-api/yarn.lock @@ -178,6 +178,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@material/material-color-utilities@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz#ff2a638d2db295a796fa02671410df4f4f97c33e" + integrity sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ== + "@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.5.35": version "5.6.2" resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.6.2.tgz#f98f8fea544f26fa9cda6f22ab8053ef295dc281" @@ -386,6 +391,48 @@ argparse "~1.0.9" string-argv "~0.3.1" +"@shikijs/core@1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.2.tgz#9c22bd4cc8a4d6c062461cfd35e1faa6c617ca25" + integrity sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg== + dependencies: + "@shikijs/engine-javascript" "1.22.2" + "@shikijs/engine-oniguruma" "1.22.2" + "@shikijs/types" "1.22.2" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.3" + +"@shikijs/engine-javascript@1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz#62e90dbd2ed1d78b972ad7d0a1f8ffaaf5e43279" + integrity sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw== + dependencies: + "@shikijs/types" "1.22.2" + "@shikijs/vscode-textmate" "^9.3.0" + oniguruma-to-js "0.4.3" + +"@shikijs/engine-oniguruma@1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz#b12a44e3faf486e19fbcf8952f4b56b9b9b8d9b8" + integrity sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA== + dependencies: + "@shikijs/types" "1.22.2" + "@shikijs/vscode-textmate" "^9.3.0" + +"@shikijs/types@1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.2.tgz#695a283f19963fe0638fc2646862ba5cfc4623a8" + integrity sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg== + dependencies: + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab" + integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== + "@stylistic/eslint-plugin@^2.8.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.8.0.tgz#9fcbcf8b4b27cc3867eedce37b8c8fded1010107" @@ -412,6 +459,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/http-link-header@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/http-link-header/-/http-link-header-1.0.7.tgz#bb1a1671a8c6d93717e0057072e9253113fdc875" @@ -424,6 +478,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash.omit@^4.5.9": + version "4.5.9" + resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.9.tgz#cf4744d034961406d6dc41d9cd109773a9ed8fe3" + integrity sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw== + dependencies: + "@types/lodash" "*" + "@types/lodash.pick@^4.4.9": version "4.4.9" resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.9.tgz#06f7d88faa81a6c5665584778aea7b1374a1dc5b" @@ -436,35 +497,40 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== -"@types/node@*": - version "20.14.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.12.tgz#129d7c3a822cb49fc7ff661235f19cfefd422b49" - integrity sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ== +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== dependencies: - undici-types "~5.26.4" + "@types/unist" "*" -"@types/node@^22.7.4": - version "22.7.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" - integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== +"@types/node@*", "@types/node@^22.9.3": + version "22.9.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.3.tgz#08f3d64b3bc6d74b162d36f60213e8a6704ef2b4" + integrity sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw== dependencies: - undici-types "~6.19.2" + undici-types "~6.19.8" "@types/semver@^7.5.8": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== -"@typescript-eslint/eslint-plugin@^8.8.0": - version "8.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz#b2b02a5447cdc885950eb256b3b8a97b92031bd3" - integrity sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A== +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@typescript-eslint/eslint-plugin@^8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz#c95c6521e70c8b095a684d884d96c0c1c63747d2" + integrity sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.8.0" - "@typescript-eslint/type-utils" "8.8.0" - "@typescript-eslint/utils" "8.8.0" - "@typescript-eslint/visitor-keys" "8.8.0" + "@typescript-eslint/scope-manager" "8.15.0" + "@typescript-eslint/type-utils" "8.15.0" + "@typescript-eslint/utils" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" @@ -481,6 +547,14 @@ "@typescript-eslint/visitor-keys" "8.8.0" debug "^4.3.4" +"@typescript-eslint/scope-manager@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz#28a1a0f13038f382424f45a988961acaca38f7c6" + integrity sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA== + dependencies: + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/scope-manager@8.8.0": version "8.8.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz#30b23a6ae5708bd7882e40675ef2f1b2beac741f" @@ -489,21 +563,40 @@ "@typescript-eslint/types" "8.8.0" "@typescript-eslint/visitor-keys" "8.8.0" -"@typescript-eslint/type-utils@8.8.0": - version "8.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz#a0ca1c8a90d94b101176a169d7a0958187408d33" - integrity sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q== +"@typescript-eslint/type-utils@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz#a6da0f93aef879a68cc66c73fe42256cb7426c72" + integrity sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw== dependencies: - "@typescript-eslint/typescript-estree" "8.8.0" - "@typescript-eslint/utils" "8.8.0" + "@typescript-eslint/typescript-estree" "8.15.0" + "@typescript-eslint/utils" "8.15.0" debug "^4.3.4" ts-api-utils "^1.3.0" +"@typescript-eslint/types@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.15.0.tgz#4958edf3d83e97f77005f794452e595aaf6430fc" + integrity sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ== + "@typescript-eslint/types@8.8.0": version "8.8.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.8.0.tgz#08ea5df6c01984d456056434641491fbf7a1bf43" integrity sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw== +"@typescript-eslint/typescript-estree@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz#915c94e387892b114a2a2cc0df2d7f19412c8ba7" + integrity sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg== + dependencies: + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/typescript-estree@8.8.0": version "8.8.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz#072eaab97fdb63513fabfe1cf271812affe779e3" @@ -518,15 +611,23 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.8.0", "@typescript-eslint/utils@^8.4.0": - version "8.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.8.0.tgz#bd8607e3a68c461b69169c7a5824637dc9e8b3f1" - integrity sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg== +"@typescript-eslint/utils@8.15.0", "@typescript-eslint/utils@^8.4.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.15.0.tgz#ac04679ad19252776b38b81954b8e5a65567cef6" + integrity sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.8.0" - "@typescript-eslint/types" "8.8.0" - "@typescript-eslint/typescript-estree" "8.8.0" + "@typescript-eslint/scope-manager" "8.15.0" + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/typescript-estree" "8.15.0" + +"@typescript-eslint/visitor-keys@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz#9ea5a85eb25401d2aa74ec8a478af4e97899ea12" + integrity sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q== + dependencies: + "@typescript-eslint/types" "8.15.0" + eslint-visitor-keys "^4.2.0" "@typescript-eslint/visitor-keys@8.8.0": version "8.8.0" @@ -536,7 +637,7 @@ "@typescript-eslint/types" "8.8.0" eslint-visitor-keys "^3.4.3" -"@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -837,6 +938,11 @@ caniuse-lite@^1.0.30001639, caniuse-lite@^1.0.30001646: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz#3574b22dfec54a3f3b6787331da1040fe8e763ec" integrity sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -845,6 +951,16 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -857,6 +973,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + compare-versions@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.1.tgz#7af3cc1099ba37d244b3145a9af5201b629148a9" @@ -878,9 +999,9 @@ confbox@^0.1.7: integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -960,6 +1081,18 @@ define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -987,7 +1120,7 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^4.5.0: +entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -1150,14 +1283,7 @@ eslint-import-resolver-typescript@^3.6.3: is-bun-module "^1.0.2" is-glob "^4.0.3" -eslint-module-utils@^2.8.1: - version "2.11.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz#b99b211ca4318243f09661fae088f373ad5243c4" - integrity sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ== - dependencies: - debug "^3.2.7" - -eslint-module-utils@^2.9.0: +eslint-module-utils@^2.12.0, eslint-module-utils@^2.8.1: version "2.12.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== @@ -1178,10 +1304,10 @@ eslint-plugin-compat@^6.0.1: lodash.memoize "^4.1.2" semver "^7.6.2" -eslint-plugin-import@^2.30.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" - integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== +eslint-plugin-import@^2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: "@rtsao/scc" "^1.1.0" array-includes "^3.1.8" @@ -1191,7 +1317,7 @@ eslint-plugin-import@^2.30.0: debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.9.0" + eslint-module-utils "^2.12.0" hasown "^2.0.2" is-core-module "^2.15.1" is-glob "^4.0.3" @@ -1200,6 +1326,7 @@ eslint-plugin-import@^2.30.0: object.groupby "^1.0.3" object.values "^1.2.0" semver "^6.3.1" + string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" eslint-plugin-promise@^6.0.0: @@ -1225,6 +1352,11 @@ eslint-visitor-keys@^4.0.0, eslint-visitor-keys@^4.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c" integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" @@ -1572,11 +1704,40 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hast-util-to-html@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b" + integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + http-link-header@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-1.1.3.tgz#b367b7a0ad1cf14027953f31aa1df40bb433da2a" @@ -1662,14 +1823,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" - integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== - dependencies: - hasown "^2.0.2" - -is-core-module@^2.15.1: +is-core-module@^2.13.0, is-core-module@^2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== @@ -1777,6 +1931,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isows@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + jju@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" @@ -1843,6 +2002,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + local-pkg@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" @@ -1868,6 +2034,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.omit@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== + lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -1885,6 +2056,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + magic-string@^0.30.11: version "0.30.11" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" @@ -1892,11 +2068,75 @@ magic-string@^0.30.11: dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micromark-util-character@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + micromatch@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" @@ -1912,7 +2152,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.3, minimatch@^9.0.4: +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -2026,6 +2266,13 @@ once@^1.3.0: dependencies: wrappy "1" +oniguruma-to-js@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740" + integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ== + dependencies: + regex "^4.3.2" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -2137,15 +2384,25 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -query-string@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.0.tgz#5f12a4653a4ba56021e113b5cf58e56581823e7a" - integrity sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw== +query-string@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.1.tgz#dbfebb4196aeb2919915f2b2b81b91b965cf03a0" + integrity sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg== dependencies: decode-uri-component "^0.4.1" filter-obj "^5.1.0" @@ -2156,6 +2413,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +regex@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/regex/-/regex-4.3.3.tgz#8cda73ccbdfa7c5691881d02f9bb142dba9daa6a" + integrity sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg== + regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -2304,6 +2566,18 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shiki@^1.16.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.2.tgz#ed109a3d0850504ad5a1edf8496470a2121c5b7b" + integrity sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA== + dependencies: + "@shikijs/core" "1.22.2" + "@shikijs/engine-javascript" "1.22.2" + "@shikijs/engine-oniguruma" "1.22.2" + "@shikijs/types" "1.22.2" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -2329,6 +2603,11 @@ source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + split-on-first@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" @@ -2372,6 +2651,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -2425,6 +2712,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" @@ -2496,6 +2788,29 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typedoc-material-theme@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/typedoc-material-theme/-/typedoc-material-theme-1.1.0.tgz#c2b6451ccc62a85f8e3a58d2437db5c5987fd81f" + integrity sha512-LLWGVb8w+i+QGnsu/a0JKjcuzndFQt/UeGVOQz0HFFGGocROEHv5QYudIACrj+phL2LDwH05tJx0Ob3pYYH2UA== + dependencies: + "@material/material-color-utilities" "^0.2.7" + +typedoc-plugin-valibot@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-valibot/-/typedoc-plugin-valibot-1.0.0.tgz#9a7e586d993f5e6c9efe60ab12724e4fb24f7efe" + integrity sha512-3LJSPc/aAKvxsfJx3/qgI1HBJvh4JNmtQYzcbxiQrVdiloIQwh4g0KtwvHV9CCl6gAnDxdnFMdE5gbhne2wXuw== + +typedoc@^0.26.10: + version "0.26.10" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.10.tgz#d372f171dc2c4458cbac6c473be9591042ab781d" + integrity sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw== + dependencies: + lunr "^2.3.9" + markdown-it "^14.1.0" + minimatch "^9.0.5" + shiki "^1.16.2" + yaml "^2.5.1" + typescript@5.4.2: version "5.4.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" @@ -2506,6 +2821,11 @@ typescript@^5.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + ufo@^1.5.3: version "1.5.4" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" @@ -2521,16 +2841,49 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -undici-types@~6.19.2: +undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -2556,6 +2909,22 @@ valibot@^0.42.1: resolved "https://registry.yarnpkg.com/valibot/-/valibot-0.42.1.tgz#a31183d8e9d7552f98e22ca0977172cab8815188" integrity sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw== +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + vite-plugin-dts@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-4.2.3.tgz#e0d9616eb574700111dbd19ae98e166541433263" @@ -2626,12 +2995,27 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.5.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" + integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/packages/pl-fe/CHANGELOG.md b/packages/pl-fe/CHANGELOG.md index 4cde8e7dd..a188a8934 100644 --- a/packages/pl-fe/CHANGELOG.md +++ b/packages/pl-fe/CHANGELOG.md @@ -14,6 +14,8 @@ Changes made since the project forked from Soapbox in April 2024. - Notifications of the same type and reposts of the same post are grouped client-side. - Date is displayed for notifications that are not about new posts. - Replies to your posts are displayed differently to other mentions in notification list. +- Hashtags from the last line of a post are displayed in a separate component. Adapted [from Mastodon](https://github.com/mastodon/mastodon/pull/26499). +- Native grouped notifications are used on Mastodon. **Settings:** - You can add image description to your avatar/backend, if supported by backend. @@ -27,6 +29,7 @@ Changes made since the project forked from Soapbox in April 2024. - Language detection is done client-side for composed posts, utilizing `fasttext.wasm.js`. - Draft posts. They are stored locally only and work with any backend. - New visibility scopes are supported – local-only and list-only for Pleroma. Local-only is a separate switch on GoToSocial. +- On backends that support explicit mentioning, you can choose to include mentions in your replies body. **Features:** - The most recent scrobble is displayed on user profile/card. @@ -37,6 +40,9 @@ Changes made since the project forked from Soapbox in April 2024. - Posts can be addressed to lists of users, on Pleroma. - Support for events with external registration. - Added a dedicated wrench reaction button. +- Interaction requests are supported. You can review pending requests and you get informed if your backend doesn't let you reply to a post. Supported on GoToSocial. +- Events with external sign up are supported. +- Application name used to post a status is displayed. ### Changed @@ -66,10 +72,13 @@ Changes made since the project forked from Soapbox in April 2024. - Conversations page is always displayed, even when Chats are supported. - Made it woke. - Emojis are zoomed on hover. +- Event create/edit form is now a page, instead of a modal. +- A star is used for favorite icon, instead of a heart. **Internal:** -- Migrated some local stores from Redux to Zustand. +- Migrated some local stores from Redux to Zustand. Other stores are being migrated away from `immutable`, before moving them either to Zustand or TanStack Query. - Posts are now emojified during render, instead of when inserting posts to the state. +- Barrel exports are no longer used. **Dependencies:** - `@tanstack/react-virtual` is used for list virtualization, instead of `react-virtuoso`. This improves compatibility with Ladybird browser. @@ -89,3 +98,4 @@ Changes made since the project forked from Soapbox in April 2024. - Improved regex for mentions in post composer. - Post tombstones don't interrupt status navigation with hotkeys. - Emojis are supported in poll options. +- Unsupported content types are not listed as available, when composing a post. diff --git a/packages/pl-fe/README.md b/packages/pl-fe/README.md index 8e586bea1..35f490174 100644 --- a/packages/pl-fe/README.md +++ b/packages/pl-fe/README.md @@ -1,5 +1,10 @@ `pl-fe` is a social networking client app. It works with any Mastodon API-compatible software, but it's focused on supporting alternative backends, like Pleroma or GoToSocial. +[![GitHub Repo stars](https://img.shields.io/github/stars/mkljczk/pl-fe)](https://github.com/mkljczk/pl-fe) +[![GitHub License](https://img.shields.io/github/license/mkljczk/pl-fe)](https://github.com/mkljczk/pl-fe?tab=AGPL-3.0-1-ov-file#readme) +[![Weblate project translated](https://img.shields.io/weblate/progress/pl-fe)](https://hosted.weblate.org/engage/pl-fe/) +[![Discord](https://img.shields.io/discord/1279834339470872598)](https://discord.gg/NCZZsqqgUH) + ## Try it out Want to test `pl-fe` with **any existing MastoAPI-compatible server?** Try [pl.mkljczk.pl](https://pl.mkljczk.pl) — enter your server's domain name to use `pl-fe` on any server! diff --git a/packages/pl-fe/heroku.yml b/packages/pl-fe/heroku.yml deleted file mode 100644 index 8eec25b9c..000000000 --- a/packages/pl-fe/heroku.yml +++ /dev/null @@ -1,3 +0,0 @@ -build: - docker: - web: Dockerfile diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index a61433ee9..737d853b4 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -39,37 +39,37 @@ ], "dependencies": { "@emoji-mart/data": "^1.2.1", - "@floating-ui/react": "^0.26.24", + "@floating-ui/react": "^0.26.28", "@fontsource/inter": "^5.1.0", "@fontsource/noto-sans-javanese": "^5.1.0", "@fontsource/roboto-mono": "^5.1.0", "@fontsource/tajawal": "^5.1.0", - "@lexical/clipboard": "^0.18.0", - "@lexical/code": "^0.18.0", - "@lexical/hashtag": "^0.18.0", - "@lexical/link": "^0.18.0", - "@lexical/list": "^0.18.0", - "@lexical/react": "^0.18.0", - "@lexical/rich-text": "^0.18.0", - "@lexical/selection": "^0.18.0", - "@lexical/utils": "^0.18.0", + "@lexical/clipboard": "^0.21.0", + "@lexical/code": "^0.21.0", + "@lexical/hashtag": "^0.21.0", + "@lexical/link": "^0.21.0", + "@lexical/list": "^0.21.0", + "@lexical/react": "^0.21.0", + "@lexical/rich-text": "^0.21.0", + "@lexical/selection": "^0.21.0", + "@lexical/utils": "^0.21.0", "@mkljczk/lexical-remark": "^0.4.0", "@mkljczk/react-hotkeys": "^1.2.2", "@reach/combobox": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", "@reduxjs/toolkit": "^2.0.1", - "@sentry/browser": "^8.33.0", - "@sentry/react": "^8.33.0", - "@tabler/icons": "^3.19.0", + "@sentry/browser": "^8.42.0", + "@sentry/react": "^8.42.0", + "@tabler/icons": "^3.24.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", - "@tanstack/react-query": "^5.59.0", - "@tanstack/react-virtual": "^3.10.8", + "@tanstack/react-query": "^5.59.20", + "@tanstack/react-virtual": "^3.10.9", "@twemoji/svg": "^15.0.0", "@uidotdev/usehooks": "^2.4.1", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "blurhash": "^2.0.5", "bowser": "^2.11.0", @@ -81,46 +81,46 @@ "detect-passive-events": "^2.0.0", "emoji-datasource": "15.0.1", "emoji-mart": "^5.6.0", - "escape-html": "^1.0.3", "exifr": "^7.1.3", "fasttext.wasm.js": "^1.0.0", "flexsearch": "^0.7.43", - "fuzzysort": "^3.0.2", + "fuzzysort": "^3.1.0", "graphemesplit": "^2.4.4", - "html-react-parser": "^5.1.16", - "immer": "^10.1.1", - "immutable": "^4.3.7", + "html-react-parser": "^5.1.19", + "immutable": "^5.0.3", "intersection-observer": "^0.12.2", "intl-messageformat": "^10.5.14", "intl-pluralrules": "^2.0.1", "isomorphic-dompurify": "^2.16.0", "leaflet": "^1.8.0", - "lexical": "^0.18.0", + "lexical": "^0.21.0", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.9.1", "multiselect-react-dropdown": "^2.0.25", + "mutative": "^1.1.0", + "object-to-formdata": "^4.5.1", "path-browserify": "^1.0.1", - "pl-api": "^0.1.5", + "pl-api": "^1.0.0-rc.4", "postcss": "^8.4.47", "process": "^0.11.10", "punycode": "^2.1.1", - "qrcode.react": "^4.0.1", - "query-string": "^9.1.0", + "qrcode.react": "^4.1.0", + "query-string": "^9.1.1", "react": "^18.3.1", "react-color": "^2.19.3", - "react-datepicker": "^7.4.0", + "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", - "react-error-boundary": "^4.0.11", + "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", "react-hot-toast": "^2.4.0", - "react-inlinesvg": "^4.0.0", + "react-inlinesvg": "^4.1.5", "react-intl": "^6.7.0", "react-motion": "^0.5.2", "react-redux": "^9.0.4", "react-router-dom": "^5.3.4", - "react-router-dom-v5-compat": "^6.26.2", + "react-router-dom-v5-compat": "^6.28.0", "react-router-scroll-4": "^1.0.0-beta.2", "react-simple-pull-to-refresh": "^1.3.3", "react-sparklines": "^1.7.0", @@ -133,47 +133,46 @@ "sass": "^1.79.4", "stringz": "^2.1.0", "tiny-queue": "^0.2.1", - "tslib": "^2.7.0", - "type-fest": "^4.26.1", - "typescript": "^5.6.2", + "tslib": "^2.8.1", + "type-fest": "^4.30.0", + "typescript": "^5.7.2", "util": "^0.12.5", "valibot": "^0.42.1", - "vite": "^5.4.8", - "vite-plugin-compile-time": "^0.2.1", + "vite": "^6.0.2", + "vite-plugin-compile-time": "^0.3.2", "vite-plugin-html": "^3.2.2", "vite-plugin-require": "^1.2.14", - "vite-plugin-static-copy": "^1.0.6", - "zustand": "^5.0.0-rc.2" + "vite-plugin-static-copy": "^2.2.0", + "zustand": "^5.0.2", + "zustand-mutative": "^1.1.0" }, "devDependencies": { - "@formatjs/cli": "^6.2.12", + "@formatjs/cli": "^6.3.11", "@jedmao/redux-mock-store": "^3.0.5", - "@stylistic/eslint-plugin": "^2.8.0", + "@sentry/types": "^8.42.0", + "@stylistic/eslint-plugin": "^2.11.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", - "@types/escape-html": "^1.0.4", "@types/leaflet": "^1.9.12", - "@types/lodash": "^4.17.9", - "@types/object-assign": "^4.0.33", + "@types/lodash": "^4.17.13", "@types/path-browserify": "^1.0.3", - "@types/react": "^18.3.11", + "@types/react": "^18.3.13", "@types/react-color": "^3.0.12", - "@types/react-dom": "^18.3.0", + "@types/react-dom": "^18.3.1", "@types/react-motion": "^0.0.40", "@types/react-router-dom": "^5.3.3", "@types/react-sparklines": "^1.7.5", "@types/react-swipeable-views": "^0.13.5", "@types/redux-mock-store": "^1.0.6", - "@types/semver": "^7.5.8", - "@typescript-eslint/eslint-plugin": "^8.8.0", - "@typescript-eslint/parser": "^8.8.0", + "@typescript-eslint/eslint-plugin": "^8.17.0", + "@typescript-eslint/parser": "^8.17.0", "eslint": "^8.57.1", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-compat": "^6.0.1", "eslint-plugin-formatjs": "4.14.0", - "eslint-plugin-import": "^2.30.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.3.1", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-promise": "^7.1.0", @@ -181,17 +180,17 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-tailwindcss": "^3.17.4", "fake-indexeddb": "^6.0.0", - "globals": "^15.10.0", - "husky": "^9.1.6", - "jsdom": "^24.0.0", + "globals": "^15.13.0", + "husky": "^9.1.7", + "jsdom": "^25.0.1", "lint-staged": ">=10", - "rollup-plugin-bundle-stats": "^4.16.0", + "rollup-plugin-bundle-stats": "^4.17.0", "stylelint": "^16.9.0", "stylelint-config-standard-scss": "^12.0.0", - "tailwindcss": "^3.4.13", + "tailwindcss": "^3.4.16", "vite-plugin-checker": "^0.8.0", - "vite-plugin-pwa": "^0.20.5", - "vitest": "^2.1.1" + "vite-plugin-pwa": "^0.21.1", + "vitest": "^2.1.8" }, "resolutions": { "@types/react": "^18.3.11", diff --git a/packages/pl-fe/src/actions/about.test.ts b/packages/pl-fe/src/actions/about.test.ts deleted file mode 100644 index edc1c939c..000000000 --- a/packages/pl-fe/src/actions/about.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import MockAdapter from 'axios-mock-adapter'; -import { Map as ImmutableMap } from 'immutable'; - -// import { staticClient } from 'pl-fe/api'; -import { mockStore } from 'pl-fe/jest/test-helpers'; - -import { - FETCH_ABOUT_PAGE_REQUEST, - // FETCH_ABOUT_PAGE_SUCCESS, - FETCH_ABOUT_PAGE_FAIL, - fetchAboutPage, -} from './about'; - -describe('fetchAboutPage()', () => { - // it('creates the expected actions on success', () => { - - // const mock = new MockAdapter(staticClient); - - // mock.onGet('/instance/about/index.html') - // .reply(200, '

Hello world

'); - - // const expectedActions = [ - // { type: FETCH_ABOUT_PAGE_REQUEST, slug: 'index' }, - // { type: FETCH_ABOUT_PAGE_SUCCESS, slug: 'index', html: '

Hello world

' }, - // ]; - // const store = mockStore(ImmutableMap()); - - // return store.dispatch(fetchAboutPage()).then(() => { - // expect(store.getActions()).toEqual(expectedActions); - // }); - // }); - - it('creates the expected actions on failure', () => { - const expectedActions = [ - { type: FETCH_ABOUT_PAGE_REQUEST, slug: 'asdf' }, - { type: FETCH_ABOUT_PAGE_FAIL, slug: 'asdf', error: new Error('Request failed with status code 404') }, - ]; - const store = mockStore(ImmutableMap()); - - return store.dispatch(fetchAboutPage('asdf')).catch(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); -}); diff --git a/packages/pl-fe/src/actions/about.ts b/packages/pl-fe/src/actions/about.ts deleted file mode 100644 index 6737b4508..000000000 --- a/packages/pl-fe/src/actions/about.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { staticFetch } from '../api'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST' as const; -const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS' as const; -const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL' as const; - -const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale }); - - const filename = `${slug}${locale ? `.${locale}` : ''}.html`; - return staticFetch(`/instance/about/${filename}`) - .then(({ data: html }) => { - dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html }); - return html; - }) - .catch(error => { - dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error }); - throw error; - }); -}; - -export { - fetchAboutPage, - FETCH_ABOUT_PAGE_REQUEST, - FETCH_ABOUT_PAGE_SUCCESS, - FETCH_ABOUT_PAGE_FAIL, -}; diff --git a/packages/pl-fe/src/actions/accounts.test.ts b/packages/pl-fe/src/actions/accounts.test.ts index ef59f6708..8b6b5f10b 100644 --- a/packages/pl-fe/src/actions/accounts.test.ts +++ b/packages/pl-fe/src/actions/accounts.test.ts @@ -4,23 +4,17 @@ import { __stub } from 'pl-fe/api'; import { buildInstance, buildRelationship } from 'pl-fe/jest/factory'; import { mockStore, rootState } from 'pl-fe/jest/test-helpers'; import { normalizeAccount } from 'pl-fe/normalizers/account'; -import { ListRecord, ReducerRecord } from 'pl-fe/reducers/user-lists'; import { - authorizeFollowRequest, blockAccount, createAccount, - expandFollowRequests, fetchAccount, fetchAccountByUsername, - fetchFollowRequests, fetchRelationships, muteAccount, removeFromFollowers, - subscribeAccount, unblockAccount, unmuteAccount, - unsubscribeAccount, } from './accounts'; let store: ReturnType; @@ -940,240 +934,3 @@ describe('fetchRelationships()', () => { }); }); }); - -describe('fetchFollowRequests()', () => { - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - relationships: ImmutableMap(), - }; - - store = mockStore(state); - - __stub((mock) => { - mock.onGet('/api/v1/follow_requests').reply(200, [], { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOW_REQUESTS_FETCH_SUCCESS', - accounts: [], - next: null, - }, - ]; - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/follow_requests').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' }, - { type: 'FOLLOW_REQUESTS_FETCH_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('expandFollowRequests()', () => { - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - follow_requests: ListRecord({ - next: 'next_url', - }), - }), - }; - store = mockStore(state); - }); - - describe('when the url is null', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - follow_requests: ListRecord({ - next: null, - }), - }), - }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').reply(200, [], { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOW_REQUESTS_EXPAND_SUCCESS', - accounts: [], - next: null, - }, - ]; - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' }, - { type: 'FOLLOW_REQUESTS_EXPAND_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('authorizeFollowRequest()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/follow_requests/${id}/authorize`).reply(200); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id }, - { type: 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS', id }, - ]; - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/follow_requests/${id}/authorize`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id }, - { type: 'FOLLOW_REQUEST_AUTHORIZE_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index 770807208..0e32c425d 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -1,6 +1,12 @@ -import { PLEROMA, type UpdateNotificationSettingsParams, type Account, type CreateAccountParams, type PaginatedResponse, type Relationship } from 'pl-api'; +import { + PLEROMA, + type UpdateNotificationSettingsParams, + type CreateAccountParams, + type Relationship, +} from 'pl-api'; import { Entities } from 'pl-fe/entity-store/entities'; +import { queryClient } from 'pl-fe/queries/client'; import { selectAccount } from 'pl-fe/selectors'; import { isLoggedIn } from 'pl-fe/utils/auth'; @@ -8,62 +14,14 @@ import { getClient, type PlfeResponse } from '../api'; import { importEntities } from './importer'; -import type { Map as ImmutableMap } from 'immutable'; +import type { MinifiedSuggestion } from 'pl-fe/queries/trends/use-suggested-accounts'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; import type { AppDispatch, RootState } from 'pl-fe/store'; import type { History } from 'pl-fe/types/history'; -const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST' as const; -const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS' as const; -const ACCOUNT_CREATE_FAIL = 'ACCOUNT_CREATE_FAIL' as const; - -const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST' as const; -const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS' as const; -const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL' as const; - -const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST' as const; const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const; -const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL' as const; -const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST' as const; const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const; -const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL' as const; - -const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST' as const; -const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS' as const; -const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL' as const; - -const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST' as const; -const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS' as const; -const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL' as const; - -const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST' as const; -const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS' as const; -const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL' as const; - -const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST' as const; -const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS' as const; -const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL' as const; - -const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST' as const; -const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS' as const; -const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL' as const; - -const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST' as const; -const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS' as const; -const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL' as const; - -const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST' as const; -const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS' as const; -const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL' as const; - -const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST' as const; -const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS' as const; -const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL' as const; - -const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST' as const; -const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS' as const; -const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL' as const; const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History) => { // The client is unauthorized - redirect to login. @@ -75,15 +33,10 @@ const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History const noOp = () => new Promise(f => f(undefined)); const createAccount = (params: CreateAccountParams) => - async (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); - return getClient(getState()).settings.createAccount(params).then((token) => - dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }), - ).catch(error => { - dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); - throw error; - }); - }; + async (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).settings.createAccount(params).then((token) => + ({ params, token }), + ); const fetchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { @@ -95,15 +48,11 @@ const fetchAccount = (accountId: string) => return Promise.resolve(null); } - dispatch(fetchAccountRequest(accountId)); - return getClient(getState()).accounts.getAccount(accountId) .then(response => { dispatch(importEntities({ accounts: [response] })); - dispatch(fetchAccountSuccess(response)); }) .catch(error => { - dispatch(fetchAccountFail(accountId, error)); }); }; @@ -116,16 +65,11 @@ const fetchAccountByUsername = (username: string, history?: History) => return getClient(getState()).accounts.getAccount(username).then(response => { dispatch(fetchRelationships([response.id])); dispatch(importEntities({ accounts: [response] })); - dispatch(fetchAccountSuccess(response)); - }).catch(error => { - dispatch(fetchAccountFail(null, error)); }); } else if (features.accountLookup) { return dispatch(accountLookup(username)).then(account => { dispatch(fetchRelationships([account.id])); - dispatch(fetchAccountSuccess(account)); }).catch(error => { - dispatch(fetchAccountFail(null, error)); maybeRedirectLogin(error, history); }); } else { @@ -134,45 +78,28 @@ const fetchAccountByUsername = (username: string, history?: History) => if (found) { dispatch(fetchRelationships([found.id])); - dispatch(fetchAccountSuccess(found)); } else { throw accounts; } - }).catch(error => { - dispatch(fetchAccountFail(null, error)); }); } }; -const fetchAccountRequest = (accountId: string) => ({ - type: ACCOUNT_FETCH_REQUEST, - accountId, -}); - -const fetchAccountSuccess = (account: Account) => ({ - type: ACCOUNT_FETCH_SUCCESS, - account, -}); - -const fetchAccountFail = (accountId: string | null, error: unknown) => ({ - type: ACCOUNT_FETCH_FAIL, - accountId, - error, - skipAlert: true, -}); - const blockAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; - dispatch(blockAccountRequest(accountId)); - return getClient(getState).filtering.blockAccount(accountId) .then(response => { dispatch(importEntities({ relationships: [response] })); + + queryClient.setQueryData>(['suggestions'], suggestions => suggestions + ? suggestions.filter((suggestion) => suggestion.account_id !== accountId) + : undefined); + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers return dispatch(blockAccountSuccess(response, getState().statuses)); - }).catch(error => dispatch(blockAccountFail(error))); + }); }; const unblockAccount = (accountId: string) => @@ -185,21 +112,12 @@ const unblockAccount = (accountId: string) => }); }; -const blockAccountRequest = (accountId: string) => ({ - type: ACCOUNT_BLOCK_REQUEST, - accountId, -}); - -const blockAccountSuccess = (relationship: Relationship, statuses: ImmutableMap) => ({ +const blockAccountSuccess = (relationship: Relationship, statuses: Record) => ({ type: ACCOUNT_BLOCK_SUCCESS, relationship, statuses, }); -const blockAccountFail = (error: unknown) => ({ - type: ACCOUNT_BLOCK_FAIL, - error, -}); const muteAccount = (accountId: string, notifications?: boolean, duration = 0) => (dispatch: AppDispatch, getState: () => RootState) => { @@ -207,8 +125,6 @@ const muteAccount = (accountId: string, notifications?: boolean, duration = 0) = const client = getClient(getState); - dispatch(muteAccountRequest(accountId)); - const params: Record = { notifications, }; @@ -226,10 +142,14 @@ const muteAccount = (accountId: string, notifications?: boolean, duration = 0) = return client.filtering.muteAccount(accountId, params) .then(response => { dispatch(importEntities({ relationships: [response] })); + + queryClient.setQueryData>(['suggestions'], suggestions => suggestions + ? suggestions.filter((suggestion) => suggestion.account_id !== accountId) + : undefined); + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers return dispatch(muteAccountSuccess(response, getState().statuses)); - }) - .catch(error => dispatch(muteAccountFail(accountId, error))); + }); }; const unmuteAccount = (accountId: string) => @@ -240,23 +160,12 @@ const unmuteAccount = (accountId: string) => .then(response => dispatch(importEntities({ relationships: [response] }))); }; -const muteAccountRequest = (accountId: string) => ({ - type: ACCOUNT_MUTE_REQUEST, - accountId, -}); - -const muteAccountSuccess = (relationship: Relationship, statuses: ImmutableMap) => ({ +const muteAccountSuccess = (relationship: Relationship, statuses: Record) => ({ type: ACCOUNT_MUTE_SUCCESS, relationship, statuses, }); -const muteAccountFail = (accountId: string, error: unknown) => ({ - type: ACCOUNT_MUTE_FAIL, - accountId, - error, -}); - const removeFromFollowers = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; @@ -280,120 +189,6 @@ const fetchRelationships = (accountIds: string[]) => .then(response => dispatch(importEntities({ relationships: response }))); }; -const fetchFollowRequests = () => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return null; - - dispatch(fetchFollowRequestsRequest()); - - return getClient(getState()).myAccount.getFollowRequests() - .then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchFollowRequestsSuccess(response.items, response.next)); - }) - .catch(error => dispatch(fetchFollowRequestsFail(error))); - }; - -const fetchFollowRequestsRequest = () => ({ - type: FOLLOW_REQUESTS_FETCH_REQUEST, -}); - -const fetchFollowRequestsSuccess = (accounts: Array, next: (() => Promise>) | null) => ({ - type: FOLLOW_REQUESTS_FETCH_SUCCESS, - accounts, - next, -}); - -const fetchFollowRequestsFail = (error: unknown) => ({ - type: FOLLOW_REQUESTS_FETCH_FAIL, - error, -}); - -const expandFollowRequests = () => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return null; - - const next = getState().user_lists.follow_requests.next; - - if (next === null) return null; - - dispatch(expandFollowRequestsRequest()); - - return next().then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(expandFollowRequestsSuccess(response.items, response.next)); - }).catch(error => dispatch(expandFollowRequestsFail(error))); - }; - -const expandFollowRequestsRequest = () => ({ - type: FOLLOW_REQUESTS_EXPAND_REQUEST, -}); - -const expandFollowRequestsSuccess = (accounts: Array, next: (() => Promise>) | null) => ({ - type: FOLLOW_REQUESTS_EXPAND_SUCCESS, - accounts, - next, -}); - -const expandFollowRequestsFail = (error: unknown) => ({ - type: FOLLOW_REQUESTS_EXPAND_FAIL, - error, -}); - -const authorizeFollowRequest = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return null; - - dispatch(authorizeFollowRequestRequest(accountId)); - - return getClient(getState()).myAccount.acceptFollowRequest(accountId) - .then(() => dispatch(authorizeFollowRequestSuccess(accountId))) - .catch(error => dispatch(authorizeFollowRequestFail(accountId, error))); - }; - -const authorizeFollowRequestRequest = (accountId: string) => ({ - type: FOLLOW_REQUEST_AUTHORIZE_REQUEST, - accountId, -}); - -const authorizeFollowRequestSuccess = (accountId: string) => ({ - type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - accountId, -}); - -const authorizeFollowRequestFail = (accountId: string, error: unknown) => ({ - type: FOLLOW_REQUEST_AUTHORIZE_FAIL, - accountId, - error, -}); - -const rejectFollowRequest = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(rejectFollowRequestRequest(accountId)); - - return getClient(getState()).myAccount.rejectFollowRequest(accountId) - .then(() => dispatch(rejectFollowRequestSuccess(accountId))) - .catch(error => dispatch(rejectFollowRequestFail(accountId, error))); - }; - -const rejectFollowRequestRequest = (accountId: string) => ({ - type: FOLLOW_REQUEST_REJECT_REQUEST, - accountId, -}); - -const rejectFollowRequestSuccess = (accountId: string) => ({ - type: FOLLOW_REQUEST_REJECT_SUCCESS, - accountId, -}); - -const rejectFollowRequestFail = (accountId: string, error: unknown) => ({ - type: FOLLOW_REQUEST_REJECT_FAIL, - accountId, - error, -}); - const pinAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp); @@ -413,184 +208,48 @@ const unpinAccount = (accountId: string) => }; const updateNotificationSettings = (params: UpdateNotificationSettingsParams) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params }); - return getClient(getState).settings.updateNotificationSettings(params).then((data) => { - dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data }); - }).catch(error => { - dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error }); - throw error; - }); - }; - -const fetchPinnedAccounts = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchPinnedAccountsRequest(accountId)); - - return getClient(getState).accounts.getAccountEndorsements(accountId).then(response => { - dispatch(importEntities({ accounts: response })); - dispatch(fetchPinnedAccountsSuccess(accountId, response, null)); - }).catch(error => { - dispatch(fetchPinnedAccountsFail(accountId, error)); - }); - }; - -const fetchPinnedAccountsRequest = (accountId: string) => ({ - type: PINNED_ACCOUNTS_FETCH_REQUEST, - accountId, -}); - -const fetchPinnedAccountsSuccess = (accountId: string, accounts: Array, next: string | null) => ({ - type: PINNED_ACCOUNTS_FETCH_SUCCESS, - accountId, - accounts, - next, -}); - -const fetchPinnedAccountsFail = (accountId: string, error: unknown) => ({ - type: PINNED_ACCOUNTS_FETCH_FAIL, - accountId, - error, -}); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.updateNotificationSettings(params).then((data) => ({ params, data })); const accountSearch = (q: string, signal?: AbortSignal) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ACCOUNT_SEARCH_REQUEST, params: { q } }); - return getClient(getState()).accounts.searchAccounts(q, { resolve: false, limit: 4, following: true }, { signal }).then((accounts) => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).accounts.searchAccounts(q, { resolve: false, limit: 4, following: true }, { signal }).then((accounts) => { dispatch(importEntities({ accounts })); - dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts }); return accounts; - }).catch(error => { - dispatch({ type: ACCOUNT_SEARCH_FAIL, skipAlert: true }); - throw error; }); - }; const accountLookup = (acct: string, signal?: AbortSignal) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct }); - return getClient(getState()).accounts.lookupAccount(acct, { signal }).then((account) => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).accounts.lookupAccount(acct, { signal }).then((account) => { if (account && account.id) dispatch(importEntities({ accounts: [account] })); - dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account }); return account; - }).catch(error => { - dispatch({ type: ACCOUNT_LOOKUP_FAIL }); - throw error; }); - }; - -const fetchBirthdayReminders = (month: number, day: number) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const me = getState().me; - - dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, accountId: me }); - - return getClient(getState).accounts.getBirthdays(day, month).then(response => { - dispatch(importEntities({ accounts: response })); - dispatch({ - type: BIRTHDAY_REMINDERS_FETCH_SUCCESS, - accounts: response, - day, - month, - accountId: me, - }); - }).catch(() => { - dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, accountId: me }); - }); - }; const biteAccount = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const client = getClient(getState); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).accounts.biteAccount(accountId); - return client.accounts.biteAccount(accountId); - }; +type AccountsAction = + | ReturnType + | ReturnType; export { - ACCOUNT_CREATE_REQUEST, - ACCOUNT_CREATE_SUCCESS, - ACCOUNT_CREATE_FAIL, - ACCOUNT_FETCH_REQUEST, - ACCOUNT_FETCH_SUCCESS, - ACCOUNT_FETCH_FAIL, - ACCOUNT_BLOCK_REQUEST, ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_BLOCK_FAIL, - ACCOUNT_MUTE_REQUEST, ACCOUNT_MUTE_SUCCESS, - ACCOUNT_MUTE_FAIL, - PINNED_ACCOUNTS_FETCH_REQUEST, - PINNED_ACCOUNTS_FETCH_SUCCESS, - PINNED_ACCOUNTS_FETCH_FAIL, - ACCOUNT_SEARCH_REQUEST, - ACCOUNT_SEARCH_SUCCESS, - ACCOUNT_SEARCH_FAIL, - ACCOUNT_LOOKUP_REQUEST, - ACCOUNT_LOOKUP_SUCCESS, - ACCOUNT_LOOKUP_FAIL, - FOLLOW_REQUESTS_FETCH_REQUEST, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_FETCH_FAIL, - FOLLOW_REQUESTS_EXPAND_REQUEST, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - FOLLOW_REQUESTS_EXPAND_FAIL, - FOLLOW_REQUEST_AUTHORIZE_REQUEST, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_AUTHORIZE_FAIL, - FOLLOW_REQUEST_REJECT_REQUEST, - FOLLOW_REQUEST_REJECT_SUCCESS, - FOLLOW_REQUEST_REJECT_FAIL, - NOTIFICATION_SETTINGS_REQUEST, - NOTIFICATION_SETTINGS_SUCCESS, - NOTIFICATION_SETTINGS_FAIL, - BIRTHDAY_REMINDERS_FETCH_REQUEST, - BIRTHDAY_REMINDERS_FETCH_SUCCESS, - BIRTHDAY_REMINDERS_FETCH_FAIL, createAccount, fetchAccount, fetchAccountByUsername, - fetchAccountRequest, - fetchAccountSuccess, - fetchAccountFail, blockAccount, unblockAccount, - blockAccountRequest, - blockAccountSuccess, - blockAccountFail, muteAccount, unmuteAccount, - muteAccountRequest, - muteAccountSuccess, - muteAccountFail, removeFromFollowers, fetchRelationships, - fetchFollowRequests, - fetchFollowRequestsRequest, - fetchFollowRequestsSuccess, - fetchFollowRequestsFail, - expandFollowRequests, - expandFollowRequestsRequest, - expandFollowRequestsSuccess, - expandFollowRequestsFail, - authorizeFollowRequest, - authorizeFollowRequestRequest, - authorizeFollowRequestSuccess, - authorizeFollowRequestFail, - rejectFollowRequest, - rejectFollowRequestRequest, - rejectFollowRequestSuccess, - rejectFollowRequestFail, pinAccount, unpinAccount, updateNotificationSettings, - fetchPinnedAccounts, - fetchPinnedAccountsRequest, - fetchPinnedAccountsSuccess, - fetchPinnedAccountsFail, accountSearch, accountLookup, - fetchBirthdayReminders, biteAccount, + type AccountsAction, }; diff --git a/packages/pl-fe/src/actions/admin.ts b/packages/pl-fe/src/actions/admin.ts index 006c3a567..afa3476aa 100644 --- a/packages/pl-fe/src/actions/admin.ts +++ b/packages/pl-fe/src/actions/admin.ts @@ -6,56 +6,24 @@ import { getClient } from '../api'; import { deleteFromTimelines } from './timelines'; -import type { Account, AdminGetAccountsParams, AdminGetReportsParams, PleromaConfig, Status } from 'pl-api'; +import type { Account, AdminAccount, AdminGetAccountsParams, AdminGetReportsParams, AdminReport, PaginatedResponse, PleromaConfig, Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST' as const; const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS' as const; -const ADMIN_CONFIG_FETCH_FAIL = 'ADMIN_CONFIG_FETCH_FAIL' as const; const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST' as const; const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS' as const; -const ADMIN_CONFIG_UPDATE_FAIL = 'ADMIN_CONFIG_UPDATE_FAIL' as const; -const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST' as const; const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS' as const; -const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL' as const; -const ADMIN_REPORT_PATCH_REQUEST = 'ADMIN_REPORT_PATCH_REQUEST' as const; const ADMIN_REPORT_PATCH_SUCCESS = 'ADMIN_REPORT_PATCH_SUCCESS' as const; -const ADMIN_REPORT_PATCH_FAIL = 'ADMIN_REPORT_PATCH_FAIL' as const; -const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST' as const; const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS' as const; -const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL' as const; -const ADMIN_USER_DELETE_REQUEST = 'ADMIN_USER_DELETE_REQUEST' as const; const ADMIN_USER_DELETE_SUCCESS = 'ADMIN_USER_DELETE_SUCCESS' as const; -const ADMIN_USER_DELETE_FAIL = 'ADMIN_USER_DELETE_FAIL' as const; const ADMIN_USER_APPROVE_REQUEST = 'ADMIN_USER_APPROVE_REQUEST' as const; const ADMIN_USER_APPROVE_SUCCESS = 'ADMIN_USER_APPROVE_SUCCESS' as const; -const ADMIN_USER_APPROVE_FAIL = 'ADMIN_USER_APPROVE_FAIL' as const; - -const ADMIN_USER_DEACTIVATE_REQUEST = 'ADMIN_USER_DEACTIVATE_REQUEST' as const; -const ADMIN_USER_DEACTIVATE_SUCCESS = 'ADMIN_USER_DEACTIVATE_SUCCESS' as const; -const ADMIN_USER_DEACTIVATE_FAIL = 'ADMIN_USER_DEACTIVATE_FAIL' as const; - -const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST' as const; -const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS' as const; -const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL' as const; - -const ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST' as const; -const ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS' as const; -const ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL' as const; - -const ADMIN_USER_TAG_REQUEST = 'ADMIN_USERS_TAG_REQUEST' as const; -const ADMIN_USER_TAG_SUCCESS = 'ADMIN_USERS_TAG_SUCCESS' as const; -const ADMIN_USER_TAG_FAIL = 'ADMIN_USERS_TAG_FAIL' as const; - -const ADMIN_USER_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST' as const; -const ADMIN_USER_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS' as const; -const ADMIN_USER_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL' as const; const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL' as const; const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST' as const; @@ -68,24 +36,18 @@ const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS' as const const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET' as const; const fetchConfig = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); - return getClient(getState).admin.config.getPleromaConfig() + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.config.getPleromaConfig() .then((data) => { - dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); - }).catch(error => { - dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error }); + dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); }); - }; const updateConfig = (configs: PleromaConfig['configs']) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); + dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); return getClient(getState).admin.config.updatePleromaConfig(configs) .then((data) => { - dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); - }).catch(error => { - dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs }); + dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); }); }; @@ -103,49 +65,28 @@ const updatePlFeConfig = (data: Record) => }; const fetchReports = (params?: AdminGetReportsParams) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); - - return getClient(state).admin.reports.getReports(params) + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.reports.getReports(params) .then(({ items }) => { items.forEach((report) => { dispatch(importEntities({ statuses: report.statuses as Array, accounts: [report.account?.account, report.target_account?.account] })); - dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports: items, params }); + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports: items, params }); }); - }).catch(error => { - dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); }); - }; const closeReport = (reportId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - dispatch({ type: ADMIN_REPORT_PATCH_REQUEST, reportId }); - - return getClient(state).admin.reports.resolveReport(reportId).then((report) => { - dispatch({ type: ADMIN_REPORT_PATCH_SUCCESS, report, reportId }); - }).catch(error => { - dispatch({ type: ADMIN_REPORT_PATCH_FAIL, error, reportId }); - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.reports.resolveReport(reportId); const fetchUsers = (params?: AdminGetAccountsParams) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); - return getClient(state).admin.accounts.getAccounts(params).then((res) => { dispatch(importEntities({ accounts: res.items.map(({ account }) => account).filter((account): account is Account => account !== null) })); dispatch(fetchRelationships(res.items.map((account) => account.id))); - dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: res.items, params, next: res.next }); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: res.items, params, next: res.next }); return res; - }).catch(error => { - dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); - throw error; }); }; @@ -153,80 +94,44 @@ const deactivateUser = (accountId: string, report_id?: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - dispatch({ type: ADMIN_USER_DEACTIVATE_REQUEST, accountId }); - return getClient(state).admin.accounts.performAccountAction(accountId, 'suspend', { report_id }); }; const deleteUser = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_USER_DELETE_REQUEST, accountId }); - - return getClient(getState).admin.accounts.deleteAccount(accountId) - .then(() => { - dispatch({ type: ADMIN_USER_DELETE_SUCCESS, accountId }); - }).catch(error => { - dispatch({ type: ADMIN_USER_DELETE_FAIL, error, accountId }); - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.deleteAccount(accountId); const approveUser = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - dispatch({ type: ADMIN_USER_APPROVE_REQUEST, accountId }); + dispatch({ type: ADMIN_USER_APPROVE_REQUEST, accountId }); - return getClient(state).admin.accounts.approveAccount(accountId) - .then((user) => { - dispatch({ type: ADMIN_USER_APPROVE_SUCCESS, user, accountId }); - }).catch(error => { - dispatch({ type: ADMIN_USER_APPROVE_FAIL, error, accountId }); - }); + return getClient(state).admin.accounts.approveAccount(accountId); }; const deleteStatus = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, statusId }); - return getClient(getState).admin.statuses.deleteStatus(statusId) + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.statuses.deleteStatus(statusId) .then(() => { dispatch(deleteFromTimelines(statusId)); - return dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, statusId }); - }).catch(error => { - return dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, statusId }); + return ({ statusId }); }); - }; const toggleStatusSensitivity = (statusId: string, sensitive: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, statusId }); - return getClient(getState).admin.statuses.updateStatus(statusId, { sensitive: !sensitive }) + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.statuses.updateStatus(statusId, { sensitive: !sensitive }) .then((status) => { dispatch(importEntities({ statuses: [status] })); - dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, statusId, status }); - }).catch(error => { - dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, error, statusId }); }); - }; const tagUser = (accountId: string, tags: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_USER_TAG_REQUEST, accountId, tags }); - return getClient(getState).admin.accounts.tagUser(accountId, tags).then(() => { - dispatch({ type: ADMIN_USER_TAG_SUCCESS, accountId, tags }); - }).catch(error => { - dispatch({ type: ADMIN_USER_TAG_FAIL, error, accountId, tags }); - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.tagUser(accountId, tags); const untagUser = (accountId: string, tags: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: ADMIN_USER_UNTAG_REQUEST, accountId, tags }); - return getClient(getState).admin.accounts.untagUser(accountId, tags).then(() => { - dispatch({ type: ADMIN_USER_UNTAG_SUCCESS, accountId, tags }); - }).catch(error => { - dispatch({ type: ADMIN_USER_UNTAG_FAIL, error, accountId, tags }); - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.untagUser(accountId, tags); /** Synchronizes user tags to the backend. */ const setTags = (accountId: string, oldTags: string[], newTags: string[]) => @@ -278,7 +183,7 @@ const fetchUserIndex = () => if (isLoading) return; - dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST }); + dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST }); const params: AdminGetAccountsParams = { origin: 'local', @@ -289,9 +194,9 @@ const fetchUserIndex = () => dispatch(fetchUsers(params)) .then((data) => { const { items, total, next } = data; - dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users: items, total, next, params }); + dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users: items, total, next, params }); }).catch(() => { - dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); + dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); }); }; @@ -301,54 +206,45 @@ const expandUserIndex = () => if (!loaded || isLoading || !next) return; - dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST }); + dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST }); next() .then((data) => { const { items, total, next } = data; - dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users: items, total, next, params }); + dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users: items, total, next, params }); }).catch(() => { - dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); + dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); }); }; +type AdminActions = + | { type: typeof ADMIN_CONFIG_FETCH_SUCCESS; configs: PleromaConfig['configs']; needsReboot: boolean } + | { type: typeof ADMIN_CONFIG_UPDATE_REQUEST; configs: PleromaConfig['configs'] } + | { type: typeof ADMIN_CONFIG_UPDATE_SUCCESS; configs: PleromaConfig['configs']; needsReboot: boolean } + | { type: typeof ADMIN_REPORTS_FETCH_SUCCESS; reports: Array; params?: AdminGetReportsParams } + | { type: typeof ADMIN_REPORT_PATCH_SUCCESS; report: AdminReport; reportId: string } + | { type: typeof ADMIN_USERS_FETCH_SUCCESS; users: Array; params?: AdminGetAccountsParams; next: (() => Promise>) | null } + | { type: typeof ADMIN_USER_DELETE_SUCCESS; accountId: string } + | { type: typeof ADMIN_USER_APPROVE_REQUEST; accountId: string } + | { type: typeof ADMIN_USER_APPROVE_SUCCESS; user: AdminAccount; accountId: string } + | ReturnType + | { type: typeof ADMIN_USER_INDEX_FETCH_REQUEST } + | { type: typeof ADMIN_USER_INDEX_FETCH_SUCCESS; users: Array; total?: number; next: (() => Promise>) | null; params?: AdminGetAccountsParams } + | { type: typeof ADMIN_USER_INDEX_FETCH_FAIL } + | { type: typeof ADMIN_USER_INDEX_EXPAND_REQUEST } + | { type: typeof ADMIN_USER_INDEX_EXPAND_SUCCESS; users: Array; total?: number; next: (() => Promise>) | null; params: AdminGetAccountsParams | null } + | { type: typeof ADMIN_USER_INDEX_EXPAND_FAIL }; + export { - ADMIN_CONFIG_FETCH_REQUEST, ADMIN_CONFIG_FETCH_SUCCESS, - ADMIN_CONFIG_FETCH_FAIL, ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS, - ADMIN_CONFIG_UPDATE_FAIL, - ADMIN_REPORTS_FETCH_REQUEST, ADMIN_REPORTS_FETCH_SUCCESS, - ADMIN_REPORTS_FETCH_FAIL, - ADMIN_REPORT_PATCH_REQUEST, ADMIN_REPORT_PATCH_SUCCESS, - ADMIN_REPORT_PATCH_FAIL, - ADMIN_USERS_FETCH_REQUEST, ADMIN_USERS_FETCH_SUCCESS, - ADMIN_USERS_FETCH_FAIL, - ADMIN_USER_DELETE_REQUEST, ADMIN_USER_DELETE_SUCCESS, - ADMIN_USER_DELETE_FAIL, ADMIN_USER_APPROVE_REQUEST, ADMIN_USER_APPROVE_SUCCESS, - ADMIN_USER_APPROVE_FAIL, - ADMIN_USER_DEACTIVATE_REQUEST, - ADMIN_USER_DEACTIVATE_SUCCESS, - ADMIN_USER_DEACTIVATE_FAIL, - ADMIN_STATUS_DELETE_REQUEST, - ADMIN_STATUS_DELETE_SUCCESS, - ADMIN_STATUS_DELETE_FAIL, - ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, - ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, - ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, - ADMIN_USER_TAG_REQUEST, - ADMIN_USER_TAG_SUCCESS, - ADMIN_USER_TAG_FAIL, - ADMIN_USER_UNTAG_REQUEST, - ADMIN_USER_UNTAG_SUCCESS, - ADMIN_USER_UNTAG_FAIL, ADMIN_USER_INDEX_EXPAND_FAIL, ADMIN_USER_INDEX_EXPAND_REQUEST, ADMIN_USER_INDEX_EXPAND_SUCCESS, @@ -367,15 +263,10 @@ export { approveUser, deleteStatus, toggleStatusSensitivity, - tagUser, - untagUser, - setTags, setBadges, - promoteToAdmin, - promoteToModerator, - demoteToUser, setRole, setUserIndexQuery, fetchUserIndex, expandUserIndex, + type AdminActions, }; diff --git a/packages/pl-fe/src/actions/aliases.ts b/packages/pl-fe/src/actions/aliases.ts index f516add23..75f56f187 100644 --- a/packages/pl-fe/src/actions/aliases.ts +++ b/packages/pl-fe/src/actions/aliases.ts @@ -11,22 +11,12 @@ import type { Account as BaseAccount } from 'pl-api'; import type { Account } from 'pl-fe/normalizers/account'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST' as const; const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS' as const; -const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL' as const; const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE' as const; const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY' as const; const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR' as const; -const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST' as const; -const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS' as const; -const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL' as const; - -const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST' as const; -const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS' as const; -const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL' as const; - const messages = defineMessages({ createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' }, removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' }, @@ -34,29 +24,18 @@ const messages = defineMessages({ const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(fetchAliasesRequest()); return getClient(getState).settings.getAccountAliases() .then(response => { dispatch(fetchAliasesSuccess(response.aliases)); - }) - .catch(err => dispatch(fetchAliasesFail(err))); + }); }; -const fetchAliasesRequest = () => ({ - type: ALIASES_FETCH_REQUEST, -}); - const fetchAliasesSuccess = (aliases: Array) => ({ type: ALIASES_FETCH_SUCCESS, value: aliases, }); -const fetchAliasesFail = (error: unknown) => ({ - type: ALIASES_FETCH_FAIL, - error, -}); - const fetchAliasesSuggestions = (q: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -86,96 +65,38 @@ const changeAliasesSuggestions = (value: string) => ({ const addToAliases = (account: Account) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(addToAliasesRequest()); return getClient(getState).settings.addAccountAlias(account.acct).then(() => { toast.success(messages.createSuccess); - dispatch(addToAliasesSuccess); dispatch(fetchAliases); - }) - .catch(err => dispatch(fetchAliasesFail(err))); + }); }; -const addToAliasesRequest = () => ({ - type: ALIASES_ADD_REQUEST, -}); - -const addToAliasesSuccess = () => ({ - type: ALIASES_ADD_SUCCESS, -}); - -const addToAliasesFail = (error: unknown) => ({ - type: ALIASES_ADD_FAIL, - error, -}); - const removeFromAliases = (account: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(addToAliasesRequest()); return getClient(getState).settings.deleteAccountAlias(account).then(() => { toast.success(messages.removeSuccess); - dispatch(removeFromAliasesSuccess); - dispatch(fetchAliases); - }).catch(err => dispatch(fetchAliasesFail(err))); + }); }; -const removeFromAliasesRequest = () => ({ - type: ALIASES_REMOVE_REQUEST, -}); - -const removeFromAliasesSuccess = () => ({ - type: ALIASES_REMOVE_SUCCESS, -}); - -const removeFromAliasesFail = (error: unknown) => ({ - type: ALIASES_REMOVE_FAIL, - error, -}); - type AliasesAction = - ReturnType | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; export { - ALIASES_FETCH_REQUEST, ALIASES_FETCH_SUCCESS, - ALIASES_FETCH_FAIL, ALIASES_SUGGESTIONS_CHANGE, ALIASES_SUGGESTIONS_READY, ALIASES_SUGGESTIONS_CLEAR, - ALIASES_ADD_REQUEST, - ALIASES_ADD_SUCCESS, - ALIASES_ADD_FAIL, - ALIASES_REMOVE_REQUEST, - ALIASES_REMOVE_SUCCESS, - ALIASES_REMOVE_FAIL, fetchAliases, - fetchAliasesRequest, - fetchAliasesSuccess, - fetchAliasesFail, fetchAliasesSuggestions, - fetchAliasesSuggestionsReady, clearAliasesSuggestions, changeAliasesSuggestions, addToAliases, - addToAliasesRequest, - addToAliasesSuccess, - addToAliasesFail, removeFromAliases, - removeFromAliasesRequest, - removeFromAliasesSuccess, - removeFromAliasesFail, type AliasesAction, }; diff --git a/packages/pl-fe/src/actions/apps.ts b/packages/pl-fe/src/actions/apps.ts index bbd5e4538..25264b910 100644 --- a/packages/pl-fe/src/actions/apps.ts +++ b/packages/pl-fe/src/actions/apps.ts @@ -10,30 +10,12 @@ import { PlApiClient, type CreateApplicationParams } from 'pl-api'; import * as BuildConfig from 'pl-fe/build-config'; -import type { AppDispatch } from 'pl-fe/store'; +const createApp = (params: CreateApplicationParams, baseURL?: string) => { + const client = new PlApiClient(baseURL || BuildConfig.BACKEND_URL || ''); -const APP_CREATE_REQUEST = 'APP_CREATE_REQUEST' as const; -const APP_CREATE_SUCCESS = 'APP_CREATE_SUCCESS' as const; -const APP_CREATE_FAIL = 'APP_CREATE_FAIL' as const; - -const createApp = (params: CreateApplicationParams, baseURL?: string) => - (dispatch: AppDispatch) => { - dispatch({ type: APP_CREATE_REQUEST, params }); - - const client = new PlApiClient(baseURL || BuildConfig.BACKEND_URL || ''); - - return client.apps.createApplication(params).then((app) => { - dispatch({ type: APP_CREATE_SUCCESS, params, app }); - return app as Record; - }).catch(error => { - dispatch({ type: APP_CREATE_FAIL, params, error }); - throw error; - }); - }; + return client.apps.createApplication(params); +}; export { - APP_CREATE_REQUEST, - APP_CREATE_SUCCESS, - APP_CREATE_FAIL, createApp, }; diff --git a/packages/pl-fe/src/actions/auth.ts b/packages/pl-fe/src/actions/auth.ts index 37241fc8f..d2a303795 100644 --- a/packages/pl-fe/src/actions/auth.ts +++ b/packages/pl-fe/src/actions/auth.ts @@ -6,7 +6,14 @@ * @see module:pl-fe/actions/oauth * @see module:pl-fe/actions/security */ -import { credentialAccountSchema, PlApiClient, type CreateAccountParams, type Token } from 'pl-api'; +import { + credentialAccountSchema, + PlApiClient, + type CreateAccountParams, + type CredentialAccount, + type CredentialApplication, + type Token, +} from 'pl-api'; import { defineMessages } from 'react-intl'; import * as v from 'valibot'; @@ -32,6 +39,7 @@ import { type PlfeResponse, getClient } from '../api'; import { importEntities } from './importer'; +import type { Account } from 'pl-fe/normalizers/account'; import type { AppDispatch, RootState } from 'pl-fe/store'; const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT' as const; @@ -45,9 +53,7 @@ const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST' as const; const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS' as const; const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL' as const; -const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST' as const; const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS' as const; -const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL' as const; const customApp = custom('app'); @@ -65,11 +71,16 @@ const createAppAndToken = () => dispatch(createAppToken()), ); +interface AuthAppCreatedAction { + type: typeof AUTH_APP_CREATED; + app: CredentialApplication; +} + /** Create an auth app, or use it from build config */ const getAuthApp = () => (dispatch: AppDispatch) => { if (customApp?.client_secret) { - return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); + return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); } else { return dispatch(createAuthApp()); } @@ -84,25 +95,31 @@ const createAuthApp = () => website: sourceCode.homepage, }; - return dispatch(createApp(params)).then((app: Record) => - dispatch({ type: AUTH_APP_CREATED, app }), + return createApp(params).then((app) => + dispatch({ type: AUTH_APP_CREATED, app }), ); }; +interface AuthAppAuthorizedAction { + type: typeof AUTH_APP_AUTHORIZED; + app: CredentialApplication; + token: Token; +} + const createAppToken = () => (dispatch: AppDispatch, getState: () => RootState) => { - const app = getState().auth.app; + const app = getState().auth.app!; const params = { - client_id: app?.client_id!, - client_secret: app?.client_secret!, - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - grant_type: 'client_credentials', - scope: getScopes(getState()), + client_id: app.client_id!, + client_secret: app.client_secret!, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + grant_type: 'client_credentials', + scope: getScopes(getState()), }; - return dispatch(obtainOAuthToken(params)).then((token: Record) => - dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), + return obtainOAuthToken(params).then((token) => + dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), ); }; @@ -111,16 +128,16 @@ const createUserToken = (username: string, password: string) => const app = getState().auth.app; const params = { - client_id: app?.client_id!, + client_id: app?.client_id!, client_secret: app?.client_secret!, - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - grant_type: 'password', - username: username, - password: password, - scope: getScopes(getState()), + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + grant_type: 'password', + username: username, + password: password, + scope: getScopes(getState()), }; - return dispatch(obtainOAuthToken(params)) + return obtainOAuthToken(params) .then((token) => dispatch(authLoggedIn(token))); }; @@ -141,17 +158,34 @@ const otpVerify = (code: string, mfa_token: string) => }).then((token) => dispatch(authLoggedIn(token))); }; +interface VerifyCredentialsRequestAction { + type: typeof VERIFY_CREDENTIALS_REQUEST; + token: string; +} + +interface VerifyCredentialsSuccessAction { + type: typeof VERIFY_CREDENTIALS_SUCCESS; + token: string; + account: CredentialAccount; +} + +interface VerifyCredentialsFailAction { + type: typeof VERIFY_CREDENTIALS_FAIL; + token: string; + error: unknown; +} + const verifyCredentials = (token: string, accountUrl?: string) => (dispatch: AppDispatch, getState: () => RootState) => { const baseURL = parseBaseURL(accountUrl) || BuildConfig.BACKEND_URL; - dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token }); + dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token }); const client = new PlApiClient(baseURL, token); return client.settings.verifyCredentials().then((account) => { dispatch(importEntities({ accounts: [account] })); - dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; }).catch(error => { @@ -160,29 +194,31 @@ const verifyCredentials = (token: string, accountUrl?: string) => const account = error.response.json; const parsedAccount = v.parse(credentialAccountSchema, error.response.json); dispatch(importEntities({ accounts: [parsedAccount] })); - dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account: parsedAccount }); + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account: parsedAccount }); if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount)); return parsedAccount; } else { if (getState().me === null) dispatch(fetchMeFail(error)); - dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); throw error; } }); }; +interface AuthAccountRememberSuccessAction { + type: typeof AUTH_ACCOUNT_REMEMBER_SUCCESS; + accountUrl: string; + account: CredentialAccount; +} + const rememberAuthAccount = (accountUrl: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl }); - return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => { + (dispatch: AppDispatch, getState: () => RootState) => + KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => { dispatch(importEntities({ accounts: [account] })); - dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl }); + dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl }); if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; - }).catch(error => { - dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true }); }); - }; const loadCredentials = (token: string, accountUrl: string) => (dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl)) @@ -204,6 +240,12 @@ const logIn = (username: string, password: string) => throw error; }); +interface AuthLoggedOutAction { + type: typeof AUTH_LOGGED_OUT; + account: Account; + standalone: boolean; +} + const logOut = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -215,7 +257,7 @@ const logOut = () => const params = { client_id: state.auth.app?.client_id!, client_secret: state.auth.app?.client_secret!, - token: state.auth.users.get(account.url)!.access_token, + token: state.auth.users[account.url]!.access_token, }; return dispatch(revokeOAuthToken(params)) @@ -227,26 +269,33 @@ const logOut = () => // Clear the account from Sentry. unsetSentryAccount(); - dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); + dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); toast.success(messages.loggedOut); }); }; -const switchAccount = (accountId: string, background = false) => +interface SwitchAccountAction { + type: typeof SWITCH_ACCOUNT; + account: Account; +} + +const switchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const account = selectAccount(getState(), accountId); + if (!account) return; + // Clear all stored cache from React Query queryClient.invalidateQueries(); queryClient.clear(); - return dispatch({ type: SWITCH_ACCOUNT, account, background }); + return dispatch({ type: SWITCH_ACCOUNT, account }); }; const fetchOwnAccounts = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - return state.auth.users.forEach((user) => { + return Object.values(state.auth.users).forEach((user) => { const account = selectAccount(state, user.id); if (!account) { dispatch(verifyCredentials(user.access_token, user.url)) @@ -270,12 +319,28 @@ const register = (params: CreateAccountParams) => const fetchCaptcha = () => (_dispatch: AppDispatch, getState: () => RootState) => getClient(getState).oauth.getCaptcha(); +interface AuthLoggedInAction { + type: typeof AUTH_LOGGED_IN; + token: Token; +} + const authLoggedIn = (token: Token) => (dispatch: AppDispatch) => { - dispatch({ type: AUTH_LOGGED_IN, token }); + dispatch({ type: AUTH_LOGGED_IN, token }); return token; }; +type AuthAction = + | SwitchAccountAction + | AuthAppCreatedAction + | AuthAppAuthorizedAction + | AuthLoggedInAction + | AuthLoggedOutAction + | VerifyCredentialsRequestAction + | VerifyCredentialsSuccessAction + | VerifyCredentialsFailAction + | AuthAccountRememberSuccessAction; + export { SWITCH_ACCOUNT, AUTH_APP_CREATED, @@ -285,13 +350,10 @@ export { VERIFY_CREDENTIALS_REQUEST, VERIFY_CREDENTIALS_SUCCESS, VERIFY_CREDENTIALS_FAIL, - AUTH_ACCOUNT_REMEMBER_REQUEST, AUTH_ACCOUNT_REMEMBER_SUCCESS, - AUTH_ACCOUNT_REMEMBER_FAIL, messages, otpVerify, verifyCredentials, - rememberAuthAccount, loadCredentials, logIn, logOut, @@ -300,4 +362,5 @@ export { register, fetchCaptcha, authLoggedIn, + type AuthAction, }; diff --git a/packages/pl-fe/src/actions/backups.ts b/packages/pl-fe/src/actions/backups.ts deleted file mode 100644 index 4a3d85994..000000000 --- a/packages/pl-fe/src/actions/backups.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getClient } from '../api'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST' as const; -const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS' as const; -const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL' as const; - -const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST' as const; -const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS' as const; -const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL' as const; - -const fetchBackups = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: BACKUPS_FETCH_REQUEST }); - - return getClient(getState).settings.getBackups().then((backups) => - dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }), - ).catch(error => { - dispatch({ type: BACKUPS_FETCH_FAIL, error }); - }); - }; - -const createBackup = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: BACKUPS_CREATE_REQUEST }); - return getClient(getState).settings.createBackup().then((backups) => - dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }), - ).catch(error => { - dispatch({ type: BACKUPS_CREATE_FAIL, error }); - }); - }; - -export { - BACKUPS_FETCH_REQUEST, - BACKUPS_FETCH_SUCCESS, - BACKUPS_FETCH_FAIL, - BACKUPS_CREATE_REQUEST, - BACKUPS_CREATE_SUCCESS, - BACKUPS_CREATE_FAIL, - fetchBackups, - createBackup, -}; diff --git a/packages/pl-fe/src/actions/bookmarks.ts b/packages/pl-fe/src/actions/bookmarks.ts index c1ff1b064..54b56ef75 100644 --- a/packages/pl-fe/src/actions/bookmarks.ts +++ b/packages/pl-fe/src/actions/bookmarks.ts @@ -17,7 +17,7 @@ const noOp = () => new Promise(f => f(undefined)); const fetchBookmarkedStatuses = (folderId?: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (getState().status_lists.get(folderId ? `bookmarks:${folderId}` : 'bookmarks')?.isLoading) { + if (getState().status_lists[folderId ? `bookmarks:${folderId}` : 'bookmarks']?.isLoading) { return dispatch(noOp); } @@ -52,9 +52,9 @@ const fetchBookmarkedStatusesFail = (error: unknown, folderId?: string) => ({ const expandBookmarkedStatuses = (folderId?: string) => (dispatch: AppDispatch, getState: () => RootState) => { const list = folderId ? `bookmarks:${folderId}` : 'bookmarks'; - const next = getState().status_lists.get(list)?.next || null; + const next = getState().status_lists[list]?.next || null; - if (next === null || getState().status_lists.get(list)?.isLoading) { + if (next === null || getState().status_lists[list]?.isLoading) { return dispatch(noOp); } @@ -102,12 +102,6 @@ export { BOOKMARKED_STATUSES_EXPAND_SUCCESS, BOOKMARKED_STATUSES_EXPAND_FAIL, fetchBookmarkedStatuses, - fetchBookmarkedStatusesRequest, - fetchBookmarkedStatusesSuccess, - fetchBookmarkedStatusesFail, expandBookmarkedStatuses, - expandBookmarkedStatusesRequest, - expandBookmarkedStatusesSuccess, - expandBookmarkedStatusesFail, type BookmarksAction, }; diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index e9dca6851..c6a65b859 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -6,6 +6,7 @@ import { isNativeEmoji } from 'pl-fe/features/emoji'; import emojiSearch from 'pl-fe/features/emoji/search'; import { Language } from 'pl-fe/features/preferences'; import { userTouching } from 'pl-fe/is-mobile'; +import { queryClient } from 'pl-fe/queries/client'; import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors'; import { tagHistory } from 'pl-fe/settings'; import { useModalsStore } from 'pl-fe/stores/modals'; @@ -13,14 +14,13 @@ import { useSettingsStore } from 'pl-fe/stores/settings'; import toast from 'pl-fe/toast'; import { isLoggedIn } from 'pl-fe/utils/auth'; -import { chooseEmoji } from './emojis'; import { importEntities } from './importer'; -import { rememberLanguageUse } from './languages'; import { uploadFile, updateMedia } from './media'; +import { saveSettings } from './settings'; import { createStatus } from './statuses'; import type { EditorState } from 'lexical'; -import type { Account as BaseAccount, BackendVersion, CreateStatusParams, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus } from 'pl-api'; +import type { Account as BaseAccount, CreateStatusParams, CustomEmoji, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus } from 'pl-api'; import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input'; import type { Emoji } from 'pl-fe/features/emoji'; import type { Account } from 'pl-fe/normalizers/account'; @@ -66,8 +66,6 @@ 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_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' 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; @@ -118,7 +116,6 @@ interface ComposeSetStatusAction { explicitAddressing: boolean; spoilerText?: string; contentType?: string | false; - v: BackendVersion; withRedraft?: boolean; draftId?: string; editorState?: string | null; @@ -135,10 +132,10 @@ const setComposeToStatus = ( editorState?: string | null, ) => (dispatch: AppDispatch, getState: () => RootState) => { - const client = getClient(getState); - const { createStatusExplicitAddressing: explicitAddressing, version: v } = client.features; + const { features } = getClient(getState); + const explicitAddressing = features.createStatusExplicitAddressing && !useSettingsStore.getState().settings.forceImplicitAddressing; - const action: ComposeSetStatusAction = { + dispatch({ type: COMPOSE_SET_STATUS, composeId: 'compose-modal', status, @@ -147,13 +144,10 @@ const setComposeToStatus = ( explicitAddressing, spoilerText, contentType, - v, withRedraft, draftId, editorState, - }; - - dispatch(action); + }); }; const changeCompose = (composeId: string, text: string) => ({ @@ -170,25 +164,27 @@ interface ComposeReplyAction { explicitAddressing: boolean; preserveSpoilers: boolean; rebloggedBy?: Pick; + approvalRequired?: boolean; } const replyCompose = ( status: ComposeReplyAction['status'], rebloggedBy?: ComposeReplyAction['rebloggedBy'], + approvalRequired?: ComposeReplyAction['approvalRequired'], ) => (dispatch: AppDispatch, getState: () => RootState) => { if (!userTouching.matches) { return window.open(`/compose?in_reply_to=${status.id}`, 'targetWindow', 'height=500,width=700'); } const state = getState(); - const client = getClient(state); - const { createStatusExplicitAddressing: explicitAddressing } = client.features; - const preserveSpoilers = useSettingsStore.getState().settings.preserveSpoilers; + const { features } = getClient(getState); + const { forceImplicitAddressing, preserveSpoilers } = useSettingsStore.getState().settings; + const explicitAddressing = features.createStatusExplicitAddressing && !forceImplicitAddressing; const account = selectOwnAccount(state); if (!account) return; - const action: ComposeReplyAction = { + dispatch({ type: COMPOSE_REPLY, composeId: 'compose-modal', status, @@ -196,9 +192,8 @@ const replyCompose = ( explicitAddressing, preserveSpoilers, rebloggedBy, - }; - - dispatch(action); + approvalRequired, + }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -221,17 +216,16 @@ const quoteCompose = (status: ComposeQuoteAction['status']) => return window.open(`/compose?quote=${status.id}`, 'targetWindow', 'height=500,width=700'); } const state = getState(); - const { createStatusExplicitAddressing: explicitAddressing } = state.auth.client.features; + const { forceImplicitAddressing } = useSettingsStore.getState().settings; + const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; - const action: ComposeQuoteAction = { + dispatch({ type: COMPOSE_QUOTE, composeId: 'compose-modal', status, account: selectOwnAccount(state), explicitAddressing, - }; - - dispatch(action); + }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -263,13 +257,11 @@ const mentionCompose = (account: ComposeMentionAction['account']) => (dispatch: AppDispatch, getState: () => RootState) => { if (!getState().me) return; - const action: ComposeMentionAction = { + dispatch({ type: COMPOSE_MENTION, composeId: 'compose-modal', account: account, - }; - - dispatch(action); + }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -281,13 +273,11 @@ interface ComposeDirectAction { const directCompose = (account: ComposeDirectAction['account']) => (dispatch: AppDispatch) => { - const action: ComposeDirectAction = { + dispatch({ type: COMPOSE_DIRECT, composeId: 'compose-modal', account, - }; - - dispatch(action); + }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -296,13 +286,11 @@ const directComposeById = (accountId: string) => const account = selectAccount(getState(), accountId); if (!account) return; - const action: ComposeDirectAction = { + dispatch({ type: COMPOSE_DIRECT, composeId: 'compose-modal', account, - }; - - dispatch(action); + }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -312,7 +300,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - const draftId = getState().compose.get(composeId)!.draft_id; + const draftId = getState().compose[composeId]!.draft_id; dispatch(submitComposeSuccess(composeId, data, accountUrl, draftId)); let toastMessage; @@ -322,7 +310,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c toastMessage = edit ? messages.editSuccess : messages.success; toastOpts = { actionLabel: messages.view, - actionLink: `/@${data.account.acct}/posts/${data.id}`, + actionLink: data.visibility === 'direct' ? '/conversations' : `/@${data.account.acct}/posts/${data.id}`, }; } else { toastMessage = messages.scheduledSuccess; @@ -336,16 +324,16 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c }; const needsDescriptions = (state: RootState, composeId: string) => { - const media = state.compose.get(composeId)!.media_attachments; + const media = state.compose[composeId]!.media_attachments; const missingDescriptionModal = useSettingsStore.getState().settings.missingDescriptionModal; - const hasMissing = media.filter(item => !item.description).size > 0; + const hasMissing = media.filter(item => !item.description).length > 0; return missingDescriptionModal && hasMissing; }; const validateSchedule = (state: RootState, composeId: string) => { - const schedule = state.compose.get(composeId)?.schedule; + const schedule = state.compose[composeId]?.schedule; if (!schedule) return true; const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); @@ -362,17 +350,19 @@ interface SubmitComposeOpts { const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => async (dispatch: AppDispatch, getState: () => RootState) => { - const { history, force = false, onSuccess, propagate } = opts; + const { force = false, onSuccess, propagate } = opts; if (!isLoggedIn(getState)) return; const state = getState(); - const compose = state.compose.get(composeId)!; + const compose = state.compose[composeId]!; const status = compose.text; const media = compose.media_attachments; const statusId = compose.id; let to = compose.to; + const { forceImplicitAddressing } = useSettingsStore.getState().settings; + const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; if (!validateSchedule(state, composeId)) { @@ -380,7 +370,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => return; } - if ((!status || !status.length) && media.size === 0) { + if ((!status || !status.length) && media.length === 0) { return; } @@ -398,14 +388,15 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => 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 = to.union(mentions.map(mention => mention.replace(/ /g, '').trim().slice(1))); + to = [...new Set([...to, ...mentions.map(mention => mention.replace(/ /g, '').trim().slice(1))])]; } dispatch(submitComposeRequest(composeId)); useModalsStore.getState().closeModal('COMPOSE'); if (compose.language && !statusId) { - dispatch(rememberLanguageUse(compose.language)); + useSettingsStore.getState().rememberLanguageUse(compose.language); + dispatch(saveSettings()); } const idempotencyKey = compose.idempotencyKey; @@ -415,33 +406,33 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => status, in_reply_to_id: compose.in_reply_to || undefined, quote_id: compose.quote || undefined, - media_ids: media.map(item => item.id).toArray(), + media_ids: media.map(item => item.id), sensitive: compose.sensitive, spoiler_text: compose.spoiler_text, visibility: compose.privacy, content_type: contentType, scheduled_at: compose.schedule?.toISOString(), language: compose.language || compose.suggested_language || undefined, - to: to.size ? to.toArray() : undefined, + to: explicitAddressing && to.length ? to : undefined, local_only: !compose.federated, }; if (compose.poll) { params.poll = { - options: compose.poll.options.toArray(), + 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.toJS(), + options_map: compose.poll.options_map, }; } - if (compose.language && compose.textMap.size) { - params.status_map = compose.textMap.toJS(); + 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.toJS(); + params.spoiler_text_map = compose.spoilerTextMap; params.spoiler_text_map[compose.language] = compose.spoiler_text; } @@ -456,9 +447,6 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => } return dispatch(createStatus(params, idempotencyKey, statusId)).then((data) => { - if (!statusId && data.scheduled_at === null && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) { - history.push('/conversations'); - } handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId, propagate); onSuccess?.(); }).catch((error) => { @@ -490,11 +478,11 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => if (!isLoggedIn(getState)) return; const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; - const media = getState().compose.get(composeId)?.media_attachments; + const media = getState().compose[composeId]?.media_attachments; const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); - const mediaCount = media ? media.size : 0; + const mediaCount = media ? media.length : 0; if (files.length + mediaCount > attachmentLimit) { toast.error(messages.uploadErrorLimit); @@ -619,9 +607,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, }); }, 200, { leading: true, trailing: true }); -const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { - const state = getState(); - const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, state.custom_emojis); +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)); }; @@ -639,7 +627,7 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root const { trends } = state.auth.client.features; if (trends) { - const currentTrends = state.trends.items; + const currentTrends = queryClient.getQueryData>(['trends', 'tags']) || []; return dispatch(updateSuggestionTags(composeId, token, currentTrends)); } @@ -657,7 +645,7 @@ const fetchComposeSuggestions = (composeId: string, token: string) => (dispatch: AppDispatch, getState: () => RootState) => { switch (token[0]) { case ':': - fetchComposeSuggestionsEmojis(dispatch, getState, composeId, token); + fetchComposeSuggestionsEmojis(dispatch, composeId, token); break; case '#': fetchComposeSuggestionsTags(dispatch, getState, composeId, token); @@ -696,18 +684,19 @@ interface ComposeSuggestionSelectAction { position: number; token: string | null; completion: string; - path: Array; + path: ['spoiler_text'] | ['poll', 'options', number]; } -const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => +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' && suggestion.id) { + if (typeof suggestion === 'object' && 'id' in suggestion) { completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; startPosition = position - 1; - dispatch(chooseEmoji(suggestion)); + useSettingsStore.getState().rememberEmojiUse(suggestion); + dispatch(saveSettings()); } else if (typeof suggestion === 'string' && suggestion[0] === '#') { completion = suggestion; startPosition = position - 1; @@ -716,16 +705,14 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str startPosition = position; } - const action: ComposeSuggestionSelectAction = { + dispatch({ type: COMPOSE_SUGGESTION_SELECT, composeId, position: startPosition, token, completion, path, - }; - - dispatch(action); + }); }; const updateSuggestionTags = (composeId: string, token: string, tags: Array) => ({ @@ -744,14 +731,14 @@ const updateTagHistory = (composeId: string, tags: string[]) => ({ const insertIntoTagHistory = (composeId: string, recognizedTags: Array, text: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const oldHistory = state.compose.get(composeId)!.tagHistory; + const oldHistory = state.compose[composeId]!.tagHistory; const me = state.me; const names = recognizedTags .filter(tag => text.match(new RegExp(`#${tag.name}`, 'i'))) .map(tag => tag.name); const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1); - names.push(...intersectedOldHistory.toJS()); + names.push(...intersectedOldHistory); const newHistory = names.slice(0, 1000); @@ -806,14 +793,6 @@ const deleteComposeLanguage = (composeId: string, value: Language) => ({ value, }); -const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({ - type: COMPOSE_EMOJI_INSERT, - composeId, - position, - emoji, - needsSpace, -}); - const addPoll = (composeId: string) => ({ type: COMPOSE_POLL_ADD, composeId, @@ -885,13 +864,11 @@ const addToMentions = (composeId: string, accountId: string) => const account = selectAccount(state, accountId); if (!account) return; - const action: ComposeAddToMentionsAction = { + return dispatch({ type: COMPOSE_ADD_TO_MENTIONS, composeId, account: account.acct, - }; - - return dispatch(action); + }); }; interface ComposeRemoveFromMentionsAction { @@ -906,13 +883,11 @@ const removeFromMentions = (composeId: string, accountId: string) => const account = selectAccount(state, accountId); if (!account) return; - const action: ComposeRemoveFromMentionsAction = { + return dispatch({ type: COMPOSE_REMOVE_FROM_MENTIONS, composeId, account: account.acct, - }; - - return dispatch(action); + }); }; interface ComposeEventReplyAction { @@ -926,7 +901,8 @@ interface ComposeEventReplyAction { const eventDiscussionCompose = (composeId: string, status: ComposeEventReplyAction['status']) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const { createStatusExplicitAddressing: explicitAddressing } = state.auth.client.features; + const { forceImplicitAddressing } = useSettingsStore.getState().settings; + const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; return dispatch({ type: COMPOSE_EVENT_REPLY, @@ -1003,7 +979,6 @@ type ComposeAction = | ReturnType | ReturnType | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType @@ -1054,7 +1029,6 @@ export { COMPOSE_MODIFIED_LANGUAGE_CHANGE, COMPOSE_LANGUAGE_ADD, COMPOSE_LANGUAGE_DELETE, - COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_FAIL, @@ -1076,7 +1050,6 @@ export { COMPOSE_ADD_SUGGESTED_LANGUAGE, COMPOSE_FEDERATED_CHANGE, setComposeToStatus, - changeCompose, replyCompose, cancelReplyCompose, quoteCompose, @@ -1085,31 +1058,17 @@ export { mentionCompose, directCompose, directComposeById, - handleComposeSubmit, submitCompose, - submitComposeRequest, - submitComposeSuccess, - submitComposeFail, uploadFile, uploadCompose, changeUploadCompose, - changeUploadComposeRequest, - changeUploadComposeSuccess, - changeUploadComposeFail, - uploadComposeRequest, - uploadComposeProgress, uploadComposeSuccess, - uploadComposeFail, undoUploadCompose, groupCompose, groupComposeModal, clearComposeSuggestions, fetchComposeSuggestions, - readyComposeSuggestionsEmojis, - readyComposeSuggestionsAccounts, selectComposeSuggestion, - updateSuggestionTags, - updateTagHistory, changeComposeSpoilerness, changeComposeContentType, changeComposeSpoilerText, @@ -1118,7 +1077,6 @@ export { changeComposeModifiedLanguage, addComposeLanguage, deleteComposeLanguage, - insertEmojiCompose, addPoll, removePoll, addSchedule, @@ -1138,5 +1096,6 @@ export { addSuggestedLanguage, changeComposeFederated, type ComposeReplyAction, + type ComposeSuggestionSelectAction, type ComposeAction, }; diff --git a/packages/pl-fe/src/actions/consumer-auth.ts b/packages/pl-fe/src/actions/consumer-auth.ts index 4e66b343d..202e12c2b 100644 --- a/packages/pl-fe/src/actions/consumer-auth.ts +++ b/packages/pl-fe/src/actions/consumer-auth.ts @@ -10,7 +10,7 @@ import { createApp } from './apps'; import type { AppDispatch, RootState } from 'pl-fe/store'; const createProviderApp = () => - async(dispatch: AppDispatch, getState: () => RootState) => { + async (dispatch: AppDispatch, getState: () => RootState) => { const scopes = getScopes(getState()); const params = { @@ -20,7 +20,7 @@ const createProviderApp = () => scopes, }; - return dispatch(createApp(params)); + return createApp(params); }; const prepareRequest = (provider: string) => diff --git a/packages/pl-fe/src/actions/conversations.ts b/packages/pl-fe/src/actions/conversations.ts index 31c43b8e4..b7b1b7c9e 100644 --- a/packages/pl-fe/src/actions/conversations.ts +++ b/packages/pl-fe/src/actions/conversations.ts @@ -21,10 +21,15 @@ 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({ + dispatch({ type: CONVERSATIONS_READ, conversationId, }); @@ -71,18 +76,32 @@ const expandConversationsFail = (error: unknown) => ({ 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({ + return dispatch({ type: CONVERSATIONS_UPDATE, conversation, }); }; +type ConversationsAction = + | ReturnType + | ReturnType + | ConversationsReadAction + | ReturnType + | ReturnType + | ReturnType + | ConversataionsUpdateAction + export { CONVERSATIONS_MOUNT, CONVERSATIONS_UNMOUNT, @@ -95,8 +114,6 @@ export { unmountConversations, markConversationRead, expandConversations, - expandConversationsRequest, - expandConversationsSuccess, - expandConversationsFail, updateConversations, + type ConversationsAction, }; diff --git a/packages/pl-fe/src/actions/custom-emojis.ts b/packages/pl-fe/src/actions/custom-emojis.ts deleted file mode 100644 index f5733b8fc..000000000 --- a/packages/pl-fe/src/actions/custom-emojis.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getClient } from '../api'; - -import type { CustomEmoji } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST' as const; -const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS' as const; -const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL' as const; - -const fetchCustomEmojis = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - if (!me) return; - - dispatch(fetchCustomEmojisRequest()); - - return getClient(getState()).instance.getCustomEmojis().then(response => { - dispatch(fetchCustomEmojisSuccess(response)); - }).catch(error => { - dispatch(fetchCustomEmojisFail(error)); - }); - }; - -const fetchCustomEmojisRequest = () => ({ - type: CUSTOM_EMOJIS_FETCH_REQUEST, -}); - -const fetchCustomEmojisSuccess = (custom_emojis: Array) => ({ - type: CUSTOM_EMOJIS_FETCH_SUCCESS, - custom_emojis, -}); - -const fetchCustomEmojisFail = (error: unknown) => ({ - type: CUSTOM_EMOJIS_FETCH_FAIL, - error, -}); - -type CustomEmojisAction = - ReturnType - | ReturnType - | ReturnType; - -export { - CUSTOM_EMOJIS_FETCH_REQUEST, - CUSTOM_EMOJIS_FETCH_SUCCESS, - CUSTOM_EMOJIS_FETCH_FAIL, - fetchCustomEmojis, - fetchCustomEmojisRequest, - fetchCustomEmojisSuccess, - fetchCustomEmojisFail, - type CustomEmojisAction, -}; diff --git a/packages/pl-fe/src/actions/directory.ts b/packages/pl-fe/src/actions/directory.ts deleted file mode 100644 index 58398b47d..000000000 --- a/packages/pl-fe/src/actions/directory.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { getClient } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; - -import type { Account, ProfileDirectoryParams } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST' as const; -const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS' as const; -const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL' as const; - -const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST' as const; -const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS' as const; -const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL' as const; - -const fetchDirectory = (params: ProfileDirectoryParams) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchDirectoryRequest()); - - return getClient(getState()).instance.profileDirectory({ ...params, limit: 20 }).then((data) => { - dispatch(importEntities({ accounts: data })); - dispatch(fetchDirectorySuccess(data)); - dispatch(fetchRelationships(data.map((x) => x.id))); - }).catch(error => dispatch(fetchDirectoryFail(error))); - }; - -const fetchDirectoryRequest = () => ({ - type: DIRECTORY_FETCH_REQUEST, -}); - -const fetchDirectorySuccess = (accounts: Array) => ({ - type: DIRECTORY_FETCH_SUCCESS, - accounts, -}); - -const fetchDirectoryFail = (error: unknown) => ({ - type: DIRECTORY_FETCH_FAIL, - error, -}); - -const expandDirectory = (params: Record) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(expandDirectoryRequest()); - - const loadedItems = getState().user_lists.directory.items.size; - - return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => { - dispatch(importEntities({ accounts: data })); - dispatch(expandDirectorySuccess(data)); - dispatch(fetchRelationships(data.map((x) => x.id))); - }).catch(error => dispatch(expandDirectoryFail(error))); - }; - -const expandDirectoryRequest = () => ({ - type: DIRECTORY_EXPAND_REQUEST, -}); - -const expandDirectorySuccess = (accounts: Array) => ({ - type: DIRECTORY_EXPAND_SUCCESS, - accounts, -}); - -const expandDirectoryFail = (error: unknown) => ({ - type: DIRECTORY_EXPAND_FAIL, - error, -}); - -type DirectoryAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export { - DIRECTORY_FETCH_REQUEST, - DIRECTORY_FETCH_SUCCESS, - DIRECTORY_FETCH_FAIL, - DIRECTORY_EXPAND_REQUEST, - DIRECTORY_EXPAND_SUCCESS, - DIRECTORY_EXPAND_FAIL, - fetchDirectory, - fetchDirectoryRequest, - fetchDirectorySuccess, - fetchDirectoryFail, - expandDirectory, - expandDirectoryRequest, - expandDirectorySuccess, - expandDirectoryFail, - type DirectoryAction, -}; diff --git a/packages/pl-fe/src/actions/domain-blocks.ts b/packages/pl-fe/src/actions/domain-blocks.ts index a943b8098..32942685b 100644 --- a/packages/pl-fe/src/actions/domain-blocks.ts +++ b/packages/pl-fe/src/actions/domain-blocks.ts @@ -1,4 +1,5 @@ import { Entities } from 'pl-fe/entity-store/entities'; +import { queryClient } from 'pl-fe/queries/client'; import { isLoggedIn } from 'pl-fe/utils/auth'; import { getClient } from '../api'; @@ -6,118 +7,63 @@ import { getClient } from '../api'; import type { PaginatedResponse } from 'pl-api'; import type { EntityStore } from 'pl-fe/entity-store/types'; import type { Account } from 'pl-fe/normalizers/account'; +import type { MinifiedSuggestion } from 'pl-fe/queries/trends/use-suggested-accounts'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST' as const; -const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS' as const; -const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL' as const; - -const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST' as const; const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS' as const; -const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL' as const; -const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST' as const; const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS' as const; -const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL' as const; -const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST' as const; const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS' as const; -const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL' as const; const blockDomain = (domain: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(blockDomainRequest(domain)); - return getClient(getState).filtering.blockDomain(domain).then(() => { // TODO: Update relationships on block const accounts = selectAccountsByDomain(getState(), domain); if (!accounts) return; - dispatch(blockDomainSuccess(domain, accounts)); - }).catch(err => { - dispatch(blockDomainFail(domain, err)); + + queryClient.setQueryData>(['suggestions'], suggestions => suggestions + ? suggestions.filter((suggestion) => !accounts.includes(suggestion.account_id)) + : undefined); }); }; -const blockDomainRequest = (domain: string) => ({ - type: DOMAIN_BLOCK_REQUEST, - domain, -}); - -const blockDomainSuccess = (domain: string, accounts: string[]) => ({ - type: DOMAIN_BLOCK_SUCCESS, - domain, - accounts, -}); - -const blockDomainFail = (domain: string, error: unknown) => ({ - type: DOMAIN_BLOCK_FAIL, - domain, - error, -}); - const unblockDomain = (domain: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(unblockDomainRequest(domain)); - return getClient(getState).filtering.unblockDomain(domain).then(() => { // TODO: Update relationships on unblock const accounts = selectAccountsByDomain(getState(), domain); if (!accounts) return; dispatch(unblockDomainSuccess(domain, accounts)); - }).catch(err => { - dispatch(unblockDomainFail(domain, err)); - }); + }).catch(() => {}); }; -const unblockDomainRequest = (domain: string) => ({ - type: DOMAIN_UNBLOCK_REQUEST, - domain, -}); - const unblockDomainSuccess = (domain: string, accounts: string[]) => ({ type: DOMAIN_UNBLOCK_SUCCESS, domain, accounts, }); -const unblockDomainFail = (domain: string, error: unknown) => ({ - type: DOMAIN_UNBLOCK_FAIL, - domain, - error, -}); - const fetchDomainBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(fetchDomainBlocksRequest()); - return getClient(getState).filtering.getDomainBlocks().then(response => { dispatch(fetchDomainBlocksSuccess(response.items, response.next)); - }).catch(err => { - dispatch(fetchDomainBlocksFail(err)); }); }; -const fetchDomainBlocksRequest = () => ({ - type: DOMAIN_BLOCKS_FETCH_REQUEST, -}); - const fetchDomainBlocksSuccess = (domains: string[], next: (() => Promise>) | null) => ({ type: DOMAIN_BLOCKS_FETCH_SUCCESS, domains, next, }); -const fetchDomainBlocksFail = (error: unknown) => ({ - type: DOMAIN_BLOCKS_FETCH_FAIL, - error, -}); - const expandDomainBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -126,13 +72,9 @@ const expandDomainBlocks = () => if (!next) return; - dispatch(expandDomainBlocksRequest()); - next().then(response => { dispatch(expandDomainBlocksSuccess(response.items, response.next)); - }).catch(err => { - dispatch(expandDomainBlocksFail(err)); - }); + }).catch(() => {}); }; const selectAccountsByDomain = (state: RootState, domain: string): string[] => { @@ -144,63 +86,24 @@ const selectAccountsByDomain = (state: RootState, domain: string): string[] => { return accounts || []; }; -const expandDomainBlocksRequest = () => ({ - type: DOMAIN_BLOCKS_EXPAND_REQUEST, -}); - const expandDomainBlocksSuccess = (domains: string[], next: (() => Promise>) | null) => ({ type: DOMAIN_BLOCKS_EXPAND_SUCCESS, domains, next, }); -const expandDomainBlocksFail = (error: unknown) => ({ - type: DOMAIN_BLOCKS_EXPAND_FAIL, - error, -}); - type DomainBlocksAction = - ReturnType - | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; + | ReturnType; export { - DOMAIN_BLOCK_REQUEST, - DOMAIN_BLOCK_SUCCESS, - DOMAIN_BLOCK_FAIL, - DOMAIN_UNBLOCK_REQUEST, DOMAIN_UNBLOCK_SUCCESS, - DOMAIN_UNBLOCK_FAIL, - DOMAIN_BLOCKS_FETCH_REQUEST, DOMAIN_BLOCKS_FETCH_SUCCESS, - DOMAIN_BLOCKS_FETCH_FAIL, - DOMAIN_BLOCKS_EXPAND_REQUEST, DOMAIN_BLOCKS_EXPAND_SUCCESS, - DOMAIN_BLOCKS_EXPAND_FAIL, blockDomain, - blockDomainRequest, - blockDomainSuccess, - blockDomainFail, unblockDomain, - unblockDomainRequest, - unblockDomainSuccess, - unblockDomainFail, fetchDomainBlocks, - fetchDomainBlocksRequest, - fetchDomainBlocksSuccess, - fetchDomainBlocksFail, expandDomainBlocks, - expandDomainBlocksRequest, - expandDomainBlocksSuccess, - expandDomainBlocksFail, type DomainBlocksAction, }; diff --git a/packages/pl-fe/src/actions/draft-statuses.ts b/packages/pl-fe/src/actions/draft-statuses.ts index a4f786765..a009fc5d9 100644 --- a/packages/pl-fe/src/actions/draft-statuses.ts +++ b/packages/pl-fe/src/actions/draft-statuses.ts @@ -3,58 +3,83 @@ import { makeGetAccount } from 'pl-fe/selectors'; import KVStore from 'pl-fe/storage/kv-store'; import type { AppDispatch, RootState } from 'pl-fe/store'; +import type { APIEntity } from 'pl-fe/types/entities'; -const DRAFT_STATUSES_FETCH_SUCCESS = 'DRAFT_STATUSES_FETCH_SUCCESS'; +const DRAFT_STATUSES_FETCH_SUCCESS = 'DRAFT_STATUSES_FETCH_SUCCESS' as const; -const PERSIST_DRAFT_STATUS = 'PERSIST_DRAFT_STATUS'; -const CANCEL_DRAFT_STATUS = 'DELETE_DRAFT_STATUS'; +const PERSIST_DRAFT_STATUS = 'PERSIST_DRAFT_STATUS' as const; +const CANCEL_DRAFT_STATUS = 'DELETE_DRAFT_STATUS' as const; const getAccount = makeGetAccount(); +interface DraftStatusesFetchSuccessAction { + type: typeof DRAFT_STATUSES_FETCH_SUCCESS; + statuses: Array; +} + const fetchDraftStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - return KVStore.getItem(`drafts:${accountUrl}`).then((statuses) => { - dispatch({ - type: DRAFT_STATUSES_FETCH_SUCCESS, - statuses, - }); + return KVStore.getItem>(`drafts:${accountUrl}`).then((statuses) => { + if (statuses) { + dispatch({ + type: DRAFT_STATUSES_FETCH_SUCCESS, + statuses, + }); + } }).catch(() => {}); }; +interface PersistDraftStatusAction { + type: typeof PERSIST_DRAFT_STATUS; + status: Record; + accountUrl: string; +} + const saveDraftStatus = (composeId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - const compose = state.compose.get(composeId)!; + const compose = state.compose[composeId]!; const draft = { - ...compose.toJS(), + ...compose, draft_id: compose.draft_id || crypto.randomUUID(), }; - dispatch({ + dispatch({ type: PERSIST_DRAFT_STATUS, status: draft, accountUrl, }); }; +interface CancelDraftStatusAction { + type: typeof CANCEL_DRAFT_STATUS; + statusId: string; + accountUrl: string; +} + const cancelDraftStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - dispatch({ + dispatch({ type: CANCEL_DRAFT_STATUS, statusId, accountUrl, }); }; +type DraftStatusesAction = + | DraftStatusesFetchSuccessAction + | PersistDraftStatusAction + | CancelDraftStatusAction + export { DRAFT_STATUSES_FETCH_SUCCESS, PERSIST_DRAFT_STATUS, @@ -62,4 +87,5 @@ export { fetchDraftStatuses, saveDraftStatus, cancelDraftStatus, + type DraftStatusesAction, }; diff --git a/packages/pl-fe/src/actions/emoji-reacts.ts b/packages/pl-fe/src/actions/emoji-reacts.ts index a98a021f1..5531452fb 100644 --- a/packages/pl-fe/src/actions/emoji-reacts.ts +++ b/packages/pl-fe/src/actions/emoji-reacts.ts @@ -8,12 +8,9 @@ import type { Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST' as const; -const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS' as const; const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL' as const; const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST' as const; -const UNEMOJI_REACT_SUCCESS = 'UNEMOJI_REACT_SUCCESS' as const; -const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL' as const; const noOp = () => () => new Promise(f => f(undefined)); @@ -25,7 +22,6 @@ const emojiReact = (status: Pick, emoji: string, custom?: string) return getClient(getState).statuses.createStatusReaction(status.id, emoji).then((response) => { dispatch(importEntities({ statuses: [response] })); - dispatch(emojiReactSuccess(response, emoji)); }).catch((error) => { dispatch(emojiReactFail(status.id, emoji, error)); }); @@ -39,9 +35,6 @@ const unEmojiReact = (status: Pick, emoji: string) => return getClient(getState).statuses.deleteStatusReaction(status.id, emoji).then(response => { dispatch(importEntities({ statuses: [response] })); - dispatch(unEmojiReactSuccess(response, emoji)); - }).catch(error => { - dispatch(unEmojiReactFail(status.id, emoji, error)); }); }; @@ -52,13 +45,6 @@ const emojiReactRequest = (statusId: string, emoji: string, custom?: string) => custom, }); -const emojiReactSuccess = (status: Status, emoji: string) => ({ - type: EMOJI_REACT_SUCCESS, - status, - statusId: status.id, - emoji, -}); - const emojiReactFail = (statusId: string, emoji: string, error: unknown) => ({ type: EMOJI_REACT_FAIL, statusId, @@ -72,42 +58,16 @@ const unEmojiReactRequest = (statusId: string, emoji: string) => ({ emoji, }); -const unEmojiReactSuccess = (status: Status, emoji: string) => ({ - type: UNEMOJI_REACT_SUCCESS, - status, - statusId: status.id, - emoji, -}); - -const unEmojiReactFail = (statusId: string, emoji: string, error: unknown) => ({ - type: UNEMOJI_REACT_FAIL, - statusId, - emoji, - error, -}); - type EmojiReactsAction = | ReturnType - | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType export { EMOJI_REACT_REQUEST, - EMOJI_REACT_SUCCESS, EMOJI_REACT_FAIL, UNEMOJI_REACT_REQUEST, - UNEMOJI_REACT_SUCCESS, - UNEMOJI_REACT_FAIL, emojiReact, unEmojiReact, - emojiReactRequest, - emojiReactSuccess, - emojiReactFail, - unEmojiReactRequest, - unEmojiReactSuccess, - unEmojiReactFail, type EmojiReactsAction, }; diff --git a/packages/pl-fe/src/actions/emojis.ts b/packages/pl-fe/src/actions/emojis.ts deleted file mode 100644 index b4b949314..000000000 --- a/packages/pl-fe/src/actions/emojis.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { saveSettings } from './settings'; - -import type { Emoji } from 'pl-fe/features/emoji'; -import type { AppDispatch } from 'pl-fe/store'; - -const EMOJI_CHOOSE = 'EMOJI_CHOOSE'; - -const chooseEmoji = (emoji: Emoji) => - (dispatch: AppDispatch) => { - dispatch({ - type: EMOJI_CHOOSE, - emoji, - }); - - dispatch(saveSettings()); - }; - -export { - EMOJI_CHOOSE, - chooseEmoji, -}; diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index 8e7021e79..4d122c802 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -1,55 +1,20 @@ import { defineMessages } from 'react-intl'; import { getClient } from 'pl-fe/api'; -import { useModalsStore } from 'pl-fe/stores/modals'; import toast from 'pl-fe/toast'; import { importEntities } from './importer'; import { STATUS_FETCH_SOURCE_FAIL, STATUS_FETCH_SOURCE_REQUEST, STATUS_FETCH_SOURCE_SUCCESS } from './statuses'; -import type { Account, CreateEventParams, Location, MediaAttachment, PaginatedResponse, Status } from 'pl-api'; +import type { CreateEventParams, Location, MediaAttachment, PaginatedResponse, Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST' as const; -const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS' as const; -const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL' as const; - -const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST' as const; -const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS' as const; -const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL' as const; - const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST' as const; -const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS' as const; const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL' as const; const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST' as const; -const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS' as const; const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL' as const; -const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST' as const; -const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS' as const; -const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL' as const; - -const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST' as const; -const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS' as const; -const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL' as const; - -const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST' as const; -const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS' as const; -const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL' as const; - -const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST' as const; -const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS' as const; -const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL' as const; - -const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST' as const; -const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS' as const; -const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL' as const; - -const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST' as const; -const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS' as const; -const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL' as const; - const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const; const EVENT_FORM_SET = 'EVENT_FORM_SET' as const; @@ -74,18 +39,6 @@ const messages = defineMessages({ rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' }, }); -const locationSearch = (query: string, signal?: AbortSignal) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: LOCATION_SEARCH_REQUEST, query }); - return getClient(getState).search.searchLocation(query, { signal }).then((locations) => { - dispatch({ type: LOCATION_SEARCH_SUCCESS, locations }); - return locations; - }).catch(error => { - dispatch({ type: LOCATION_SEARCH_FAIL }); - throw error; - }); - }; - const submitEvent = ({ statusId, name, @@ -112,8 +65,6 @@ const submitEvent = ({ return; } - dispatch(submitEventRequest()); - const params: CreateEventParams = { name, status, @@ -131,9 +82,7 @@ const submitEvent = ({ ? getClient(state).events.createEvent(params) : getClient(state).events.editEvent(statusId, params) ).then((data) => { - useModalsStore.getState().closeModal('COMPOSE_EVENT'); dispatch(importEntities({ statuses: [data] })); - dispatch(submitEventSuccess(data)); toast.success( statusId ? messages.editSuccess : messages.success, { @@ -141,28 +90,14 @@ const submitEvent = ({ actionLink: `/@${data.account.acct}/events/${data.id}`, }, ); - }).catch((error) => { - dispatch(submitEventFail(error)); + + return data; }); }; -const submitEventRequest = () => ({ - type: EVENT_SUBMIT_REQUEST, -}); - -const submitEventSuccess = (status: Status) => ({ - type: EVENT_SUBMIT_SUCCESS, - status, -}); - -const submitEventFail = (error: unknown) => ({ - type: EVENT_SUBMIT_FAIL, - error, -}); - const joinEvent = (statusId: string, participationMessage?: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const status = getState().statuses.get(statusId); + const status = getState().statuses[statusId]; if (!status || !status.event || status.event.join_state) { return dispatch(noOp); @@ -172,7 +107,6 @@ const joinEvent = (statusId: string, participationMessage?: string) => return getClient(getState).events.joinEvent(statusId, participationMessage).then((data) => { dispatch(importEntities({ statuses: [data] })); - dispatch(joinEventSuccess(status.id)); toast.success( data.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess, { @@ -190,12 +124,7 @@ const joinEventRequest = (statusId: string) => ({ statusId, }); -const joinEventSuccess = (statusId: string) => ({ - type: EVENT_JOIN_SUCCESS, - statusId, -}); - -const joinEventFail = (error: unknown, statusId: string, previousState: string | null) => ({ +const joinEventFail = (error: unknown, statusId: string, previousState: Exclude['join_state'] | null) => ({ type: EVENT_JOIN_FAIL, error, statusId, @@ -204,7 +133,7 @@ const joinEventFail = (error: unknown, statusId: string, previousState: string | const leaveEvent = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const status = getState().statuses.get(statusId); + const status = getState().statuses[statusId]; if (!status || !status.event || !status.event.join_state) { return dispatch(noOp); @@ -214,9 +143,8 @@ const leaveEvent = (statusId: string) => return getClient(getState).events.leaveEvent(statusId).then((data) => { dispatch(importEntities({ statuses: [data] })); - dispatch(leaveEventSuccess(status.id)); }).catch((error) => { - dispatch(leaveEventFail(error, status.id)); + dispatch(leaveEventFail(error, status.id, status?.event?.join_state || null)); }); }; @@ -225,211 +153,11 @@ const leaveEventRequest = (statusId: string) => ({ statusId, }); -const leaveEventSuccess = (statusId: string) => ({ - type: EVENT_LEAVE_SUCCESS, - statusId, -}); - -const leaveEventFail = (error: unknown, statusId: string) => ({ +const leaveEventFail = (error: unknown, statusId: string, previousState: Exclude['join_state'] | null) => ({ type: EVENT_LEAVE_FAIL, statusId, error, -}); - -const fetchEventParticipations = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchEventParticipationsRequest(statusId)); - - return getClient(getState).events.getEventParticipations(statusId).then(response => { - dispatch(importEntities({ accounts: response.items })); - return dispatch(fetchEventParticipationsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(fetchEventParticipationsFail(statusId, error)); - }); - }; - -const fetchEventParticipationsRequest = (statusId: string) => ({ - type: EVENT_PARTICIPATIONS_FETCH_REQUEST, - statusId, -}); - -const fetchEventParticipationsSuccess = (statusId: string, accounts: Array, next: (() => Promise>) | null) => ({ - type: EVENT_PARTICIPATIONS_FETCH_SUCCESS, - statusId, - accounts, - next, -}); - -const fetchEventParticipationsFail = (statusId: string, error: unknown) => ({ - type: EVENT_PARTICIPATIONS_FETCH_FAIL, - statusId, - error, -}); - -const expandEventParticipations = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().user_lists.event_participations.get(statusId)?.next || null; - - if (next === null) { - return dispatch(noOp); - } - - dispatch(expandEventParticipationsRequest(statusId)); - - return next().then(response => { - dispatch(importEntities({ accounts: response.items })); - return dispatch(expandEventParticipationsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(expandEventParticipationsFail(statusId, error)); - }); - }; - -const expandEventParticipationsRequest = (statusId: string) => ({ - type: EVENT_PARTICIPATIONS_EXPAND_REQUEST, - statusId, -}); - -const expandEventParticipationsSuccess = (statusId: string, accounts: Array, next: (() => Promise>) | null) => ({ - type: EVENT_PARTICIPATIONS_EXPAND_SUCCESS, - statusId, - accounts, - next, -}); - -const expandEventParticipationsFail = (statusId: string, error: unknown) => ({ - type: EVENT_PARTICIPATIONS_EXPAND_FAIL, - statusId, - error, -}); - -const fetchEventParticipationRequests = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchEventParticipationRequestsRequest(statusId)); - - return getClient(getState).events.getEventParticipationRequests(statusId).then(response => { - dispatch(importEntities({ accounts: response.items.map(({ account }) => account) })); - return dispatch(fetchEventParticipationRequestsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(fetchEventParticipationRequestsFail(statusId, error)); - }); - }; - -const fetchEventParticipationRequestsRequest = (statusId: string) => ({ - type: EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST, - statusId, -}); - -const fetchEventParticipationRequestsSuccess = (statusId: string, participations: Array<{ - account: Account; - participation_message: string; -}>, next: (() => Promise>) | null) => ({ - type: EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS, - statusId, - participations, - next, -}); - -const fetchEventParticipationRequestsFail = (statusId: string, error: unknown) => ({ - type: EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL, - statusId, - error, -}); - -const expandEventParticipationRequests = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().user_lists.event_participation_requests.get(statusId)?.next || null; - - if (next === null) { - return dispatch(noOp); - } - - dispatch(expandEventParticipationRequestsRequest(statusId)); - - return next().then(response => { - dispatch(importEntities({ accounts: response.items.map(({ account }) => account) })); - return dispatch(expandEventParticipationRequestsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(expandEventParticipationRequestsFail(statusId, error)); - }); - }; - -const expandEventParticipationRequestsRequest = (statusId: string) => ({ - type: EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, - statusId, -}); - -const expandEventParticipationRequestsSuccess = (statusId: string, participations: Array<{ - account: Account; - participation_message: string; -}>, next: (() => Promise>) | null) => ({ - type: EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, - statusId, - participations, - next, -}); - -const expandEventParticipationRequestsFail = (statusId: string, error: unknown) => ({ - type: EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL, - statusId, - error, -}); - -const authorizeEventParticipationRequest = (statusId: string, accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(authorizeEventParticipationRequestRequest(statusId, accountId)); - - return getClient(getState).events.acceptEventParticipationRequest(statusId, accountId).then(() => { - dispatch(authorizeEventParticipationRequestSuccess(statusId, accountId)); - toast.success(messages.authorized); - }).catch(error => dispatch(authorizeEventParticipationRequestFail(statusId, accountId, error))); - }; - -const authorizeEventParticipationRequestRequest = (statusId: string, accountId: string) => ({ - type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST, - statusId, - accountId, -}); - -const authorizeEventParticipationRequestSuccess = (statusId: string, accountId: string) => ({ - type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS, - statusId, - accountId, -}); - -const authorizeEventParticipationRequestFail = (statusId: string, accountId: string, error: unknown) => ({ - type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL, - statusId, - accountId, - error, -}); - -const rejectEventParticipationRequest = (statusId: string, accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(rejectEventParticipationRequestRequest(statusId, accountId)); - - return getClient(getState).events.rejectEventParticipationRequest(statusId, accountId).then(() => { - dispatch(rejectEventParticipationRequestSuccess(statusId, accountId)); - toast.success(messages.rejected); - }).catch(error => dispatch(rejectEventParticipationRequestFail(statusId, accountId, error))); - }; - -const rejectEventParticipationRequestRequest = (statusId: string, accountId: string) => ({ - type: EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST, - statusId, - accountId, -}); - -const rejectEventParticipationRequestSuccess = (statusId: string, accountId: string) => ({ - type: EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS, - statusId, - accountId, -}); - -const rejectEventParticipationRequestFail = (statusId: string, accountId: string, error: unknown) => ({ - type: EVENT_PARTICIPATION_REQUEST_REJECT_FAIL, - statusId, - accountId, - error, + previousState, }); const fetchEventIcs = (statusId: string) => @@ -446,23 +174,17 @@ interface EventFormSetAction { text: string; } -const editEvent = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const status = getState().statuses.get(statusId)!; - +const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_FETCH_SOURCE_REQUEST, statusId }); return getClient(getState()).statuses.getStatusSource(statusId).then(response => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS, statusId }); - dispatch({ + dispatch({ type: EVENT_FORM_SET, composeId: `compose-event-modal-${statusId}`, text: response.text, }); - useModalsStore.getState().openModal('COMPOSE_EVENT', { - status, - statusText: response.text, - location: response.location || undefined, - }); + return response; }).catch(error => { dispatch({ type: STATUS_FETCH_SOURCE_FAIL, statusId, error }); }); @@ -470,81 +192,65 @@ const editEvent = (statusId: string) => (dispatch: AppDispatch, getState: () => const fetchRecentEvents = () => (dispatch: AppDispatch, getState: () => RootState) => { - if (getState().status_lists.get('recent_events')?.isLoading) { + if (getState().status_lists.recent_events?.isLoading) { return; } - dispatch({ type: RECENT_EVENTS_FETCH_REQUEST }); + dispatch({ type: RECENT_EVENTS_FETCH_REQUEST }); return getClient(getState()).timelines.publicTimeline({ only_events: true, }).then(response => { dispatch(importEntities({ statuses: response.items })); - dispatch({ + dispatch({ type: RECENT_EVENTS_FETCH_SUCCESS, statuses: response.items, next: response.next, }); }).catch(error => { - dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error }); + dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error }); }); }; const fetchJoinedEvents = () => (dispatch: AppDispatch, getState: () => RootState) => { - if (getState().status_lists.get('joined_events')?.isLoading) { + if (getState().status_lists.joined_events?.isLoading) { return; } - dispatch({ type: JOINED_EVENTS_FETCH_REQUEST }); + dispatch({ type: JOINED_EVENTS_FETCH_REQUEST }); getClient(getState).events.getJoinedEvents().then(response => { dispatch(importEntities({ statuses: response.items })); - dispatch({ + dispatch({ type: JOINED_EVENTS_FETCH_SUCCESS, statuses: response.items, next: response.next, }); }).catch(error => { - dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error }); + dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error }); }); }; type EventsAction = + | ReturnType + | ReturnType + | ReturnType + | ReturnType | ReturnType - | EventFormSetAction; + | EventFormSetAction + | { type: typeof RECENT_EVENTS_FETCH_REQUEST } + | { type: typeof RECENT_EVENTS_FETCH_SUCCESS; statuses: Array; next: (() => Promise>) | null } + | { type: typeof RECENT_EVENTS_FETCH_FAIL; error: unknown } + | { type: typeof JOINED_EVENTS_FETCH_REQUEST } + | { type: typeof JOINED_EVENTS_FETCH_SUCCESS; statuses: Array; next: (() => Promise>) | null } + | { type: typeof JOINED_EVENTS_FETCH_FAIL; error: unknown } export { - LOCATION_SEARCH_REQUEST, - LOCATION_SEARCH_SUCCESS, - LOCATION_SEARCH_FAIL, - EVENT_SUBMIT_REQUEST, - EVENT_SUBMIT_SUCCESS, - EVENT_SUBMIT_FAIL, EVENT_JOIN_REQUEST, - EVENT_JOIN_SUCCESS, EVENT_JOIN_FAIL, EVENT_LEAVE_REQUEST, - EVENT_LEAVE_SUCCESS, EVENT_LEAVE_FAIL, - EVENT_PARTICIPATIONS_FETCH_REQUEST, - EVENT_PARTICIPATIONS_FETCH_SUCCESS, - EVENT_PARTICIPATIONS_FETCH_FAIL, - EVENT_PARTICIPATIONS_EXPAND_REQUEST, - EVENT_PARTICIPATIONS_EXPAND_SUCCESS, - EVENT_PARTICIPATIONS_EXPAND_FAIL, - EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST, - EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS, - EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL, - EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, - EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, - EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL, - EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST, - EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS, - EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL, - EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST, - EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS, - EVENT_PARTICIPATION_REQUEST_REJECT_FAIL, EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, RECENT_EVENTS_FETCH_REQUEST, @@ -553,46 +259,12 @@ export { JOINED_EVENTS_FETCH_REQUEST, JOINED_EVENTS_FETCH_SUCCESS, JOINED_EVENTS_FETCH_FAIL, - locationSearch, submitEvent, - submitEventRequest, - submitEventSuccess, - submitEventFail, joinEvent, - joinEventRequest, - joinEventSuccess, - joinEventFail, leaveEvent, - leaveEventRequest, - leaveEventSuccess, - leaveEventFail, - fetchEventParticipations, - fetchEventParticipationsRequest, - fetchEventParticipationsSuccess, - fetchEventParticipationsFail, - expandEventParticipations, - expandEventParticipationsRequest, - expandEventParticipationsSuccess, - expandEventParticipationsFail, - fetchEventParticipationRequests, - fetchEventParticipationRequestsRequest, - fetchEventParticipationRequestsSuccess, - fetchEventParticipationRequestsFail, - expandEventParticipationRequests, - expandEventParticipationRequestsRequest, - expandEventParticipationRequestsSuccess, - expandEventParticipationRequestsFail, - authorizeEventParticipationRequest, - authorizeEventParticipationRequestRequest, - authorizeEventParticipationRequestSuccess, - authorizeEventParticipationRequestFail, - rejectEventParticipationRequest, - rejectEventParticipationRequestRequest, - rejectEventParticipationRequestSuccess, - rejectEventParticipationRequestFail, fetchEventIcs, cancelEventCompose, - editEvent, + initEventEdit, fetchRecentEvents, fetchJoinedEvents, type EventsAction, diff --git a/packages/pl-fe/src/actions/export-data.ts b/packages/pl-fe/src/actions/export-data.ts index d91b1760b..05ed669a0 100644 --- a/packages/pl-fe/src/actions/export-data.ts +++ b/packages/pl-fe/src/actions/export-data.ts @@ -5,19 +5,7 @@ import { normalizeAccount } from 'pl-fe/normalizers/account'; import toast from 'pl-fe/toast'; import type { Account, PaginatedResponse } from 'pl-api'; -import type { RootState } from 'pl-fe/store'; - -const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST' as const; -const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS' as const; -const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL' as const; - -const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST' as const; -const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS' as const; -const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL' as const; - -const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST' as const; -const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS' as const; -const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL' as const; +import type { AppDispatch, RootState } from 'pl-fe/store'; const messages = defineMessages({ blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' }, @@ -25,19 +13,6 @@ const messages = defineMessages({ mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' }, }); -type ExportDataAction = { - type: typeof EXPORT_FOLLOWS_REQUEST - | typeof EXPORT_FOLLOWS_SUCCESS - | typeof EXPORT_FOLLOWS_FAIL - | typeof EXPORT_BLOCKS_REQUEST - | typeof EXPORT_BLOCKS_SUCCESS - | typeof EXPORT_BLOCKS_FAIL - | typeof EXPORT_MUTES_REQUEST - | typeof EXPORT_MUTES_SUCCESS - | typeof EXPORT_MUTES_FAIL; - error?: any; -} - const fileExport = (content: string, fileName: string) => { const fileToDownload = document.createElement('a'); @@ -61,8 +36,7 @@ const listAccounts = async (response: PaginatedResponse) => { return Array.from(new Set(accounts)); }; -const exportFollows = () => async (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: EXPORT_FOLLOWS_REQUEST }); +const exportFollows = () => async (_dispatch: AppDispatch, getState: () => RootState) => { const me = getState().me; if (!me) return; @@ -74,52 +48,29 @@ const exportFollows = () => async (dispatch: React.Dispatch, g fileExport(followings.join('\n'), 'export_followings.csv'); toast.success(messages.followersSuccess); - dispatch({ type: EXPORT_FOLLOWS_SUCCESS }); - }).catch(error => { - dispatch({ type: EXPORT_FOLLOWS_FAIL, error }); }); }; -const exportBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: EXPORT_BLOCKS_REQUEST }); - return getClient(getState()).filtering.getBlocks({ limit: 40 }) +const exportBlocks = () => (_dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).filtering.getBlocks({ limit: 40 }) .then(listAccounts) .then((blocks) => { fileExport(blocks.join('\n'), 'export_block.csv'); toast.success(messages.blocksSuccess); - dispatch({ type: EXPORT_BLOCKS_SUCCESS }); - }).catch(error => { - dispatch({ type: EXPORT_BLOCKS_FAIL, error }); }); -}; -const exportMutes = () => (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: EXPORT_MUTES_REQUEST }); - return getClient(getState()).filtering.getMutes({ limit: 40 }) +const exportMutes = () => (_dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).filtering.getMutes({ limit: 40 }) .then(listAccounts) .then((mutes) => { fileExport(mutes.join('\n'), 'export_mutes.csv'); toast.success(messages.mutesSuccess); - dispatch({ type: EXPORT_MUTES_SUCCESS }); - }).catch(error => { - dispatch({ type: EXPORT_MUTES_FAIL, error }); }); -}; export { - EXPORT_FOLLOWS_REQUEST, - EXPORT_FOLLOWS_SUCCESS, - EXPORT_FOLLOWS_FAIL, - EXPORT_BLOCKS_REQUEST, - EXPORT_BLOCKS_SUCCESS, - EXPORT_BLOCKS_FAIL, - EXPORT_MUTES_REQUEST, - EXPORT_MUTES_SUCCESS, - EXPORT_MUTES_FAIL, exportFollows, exportBlocks, exportMutes, - type ExportDataAction, }; diff --git a/packages/pl-fe/src/actions/external-auth.ts b/packages/pl-fe/src/actions/external-auth.ts index da6f01687..2232873d0 100644 --- a/packages/pl-fe/src/actions/external-auth.ts +++ b/packages/pl-fe/src/actions/external-auth.ts @@ -46,12 +46,12 @@ const externalAuthorize = (instance: Instance, baseURL: string) => (dispatch: AppDispatch) => { const scopes = getInstanceScopes(instance); - return dispatch(createExternalApp(instance, baseURL)).then((app) => { - const { client_id, redirect_uri } = app as Record; + return createExternalApp(instance, baseURL).then((app) => { + const { client_id, redirect_uri } = app; const query = new URLSearchParams({ client_id, - redirect_uri, + redirect_uri: redirect_uri || app.redirect_uris[0]!, response_type: 'code', scope: scopes, }); @@ -88,9 +88,9 @@ const loginWithCode = (code: string) => code, }; - return dispatch(obtainOAuthToken(params, baseURL)) + return obtainOAuthToken(params, baseURL) .then((token) => dispatch(authLoggedIn(token))) - .then(({ access_token }) => dispatch(verifyCredentials(access_token as string, baseURL))) + .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) .then((account) => dispatch(switchAccount(account.id))) .then(() => window.location.href = '/'); }; diff --git a/packages/pl-fe/src/actions/familiar-followers.ts b/packages/pl-fe/src/actions/familiar-followers.ts deleted file mode 100644 index b344728e0..000000000 --- a/packages/pl-fe/src/actions/familiar-followers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AppDispatch, RootState } from 'pl-fe/store'; - -import { getClient } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; - -const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST' as const; -const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS' as const; -const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL' as const; - -const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ - type: FAMILIAR_FOLLOWERS_FETCH_REQUEST, - accountId, - }); - - getClient(getState()).accounts.getFamiliarFollowers([accountId]) - .then((data) => { - const accounts = data.find(({ id }: { id: string }) => id === accountId)!.accounts; - - dispatch(importEntities({ accounts })); - dispatch(fetchRelationships(accounts.map((item) => item.id))); - dispatch({ - type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS, - accountId, - accounts, - }); - }) - .catch(error => dispatch({ - type: FAMILIAR_FOLLOWERS_FETCH_FAIL, - accountId, - error, - skipAlert: true, - })); -}; - -export { - FAMILIAR_FOLLOWERS_FETCH_REQUEST, - FAMILIAR_FOLLOWERS_FETCH_SUCCESS, - FAMILIAR_FOLLOWERS_FETCH_FAIL, - fetchAccountFamiliarFollowers, -}; diff --git a/packages/pl-fe/src/actions/favourites.ts b/packages/pl-fe/src/actions/favourites.ts index 736a3b88a..81f9f9dd8 100644 --- a/packages/pl-fe/src/actions/favourites.ts +++ b/packages/pl-fe/src/actions/favourites.ts @@ -27,7 +27,7 @@ const fetchFavouritedStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - if (getState().status_lists.get('favourites')?.isLoading) { + if (getState().status_lists.favourites?.isLoading) { return; } @@ -60,9 +60,9 @@ const expandFavouritedStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - const next = getState().status_lists.get('favourites')?.next || null; + const next = getState().status_lists.favourites?.next || null; - if (next === null || getState().status_lists.get('favourites')?.isLoading) { + if (next === null || getState().status_lists.favourites?.isLoading) { return; } @@ -95,7 +95,7 @@ const fetchAccountFavouritedStatuses = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - if (getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { + if (getState().status_lists[`favourites:${accountId}`]?.isLoading) { return; } @@ -131,9 +131,9 @@ const expandAccountFavouritedStatuses = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - const next = getState().status_lists.get(`favourites:${accountId}`)?.next || null; + const next = getState().status_lists[`favourites:${accountId}`]?.next || null; - if (next === null || getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { + if (next === null || getState().status_lists[`favourites:${accountId}`]?.isLoading) { return; } @@ -193,20 +193,8 @@ export { ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS, ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL, fetchFavouritedStatuses, - fetchFavouritedStatusesRequest, - fetchFavouritedStatusesSuccess, - fetchFavouritedStatusesFail, expandFavouritedStatuses, - expandFavouritedStatusesRequest, - expandFavouritedStatusesSuccess, - expandFavouritedStatusesFail, fetchAccountFavouritedStatuses, - fetchAccountFavouritedStatusesRequest, - fetchAccountFavouritedStatusesSuccess, - fetchAccountFavouritedStatusesFail, expandAccountFavouritedStatuses, - expandAccountFavouritedStatusesRequest, - expandAccountFavouritedStatusesSuccess, - expandAccountFavouritedStatusesFail, type FavouritesAction, }; diff --git a/packages/pl-fe/src/actions/filters.ts b/packages/pl-fe/src/actions/filters.ts index 1499664ea..5d20b7d52 100644 --- a/packages/pl-fe/src/actions/filters.ts +++ b/packages/pl-fe/src/actions/filters.ts @@ -5,28 +5,10 @@ import { isLoggedIn } from 'pl-fe/utils/auth'; import { getClient } from '../api'; -import type { FilterContext } from 'pl-api'; +import type { Filter, FilterContext } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST' as const; const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS' as const; -const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL' as const; - -const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST' as const; -const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS' as const; -const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL' as const; - -const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST' as const; -const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS' as const; -const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL' as const; - -const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST' as const; -const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS' as const; -const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL' as const; - -const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST' as const; -const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS' as const; -const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL' as const; const messages = defineMessages({ added: { id: 'filters.added', defaultMessage: 'Filter added.' }, @@ -39,116 +21,63 @@ const fetchFilters = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch({ - type: FILTERS_FETCH_REQUEST, - }); - return getClient(getState).filtering.getFilters() - .then((data) => dispatch({ - type: FILTERS_FETCH_SUCCESS, + .then((data) => ({ filters: data, })) - .catch(err => dispatch({ - type: FILTERS_FETCH_FAIL, - err, - skipAlert: true, + .catch(error => ({ + error, })); }; const fetchFilter = (filterId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FILTER_FETCH_REQUEST }); - - return getClient(getState).filtering.getFilter(filterId) - .then((data) => { - dispatch({ - type: FILTER_FETCH_SUCCESS, - filter: data, - }); - - return data; - }) - .catch(err => { - dispatch({ - type: FILTER_FETCH_FAIL, - err, - skipAlert: true, - }); - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).filtering.getFilter(filterId); const createFilter = (title: string, expires_in: number | undefined, context: Array, hide: boolean, keywords_attributes: FilterKeywords) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FILTERS_CREATE_REQUEST }); - - return getClient(getState).filtering.createFilter({ + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).filtering.createFilter({ title, context, filter_action: hide ? 'hide' : 'warn', expires_in, keywords_attributes, }).then(response => { - dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response }); toast.success(messages.added); return response; - }).catch(error => { - dispatch({ type: FILTERS_CREATE_FAIL, error }); }); - }; const updateFilter = (filterId: string, title: string, expires_in: number | undefined, context: Array, hide: boolean, keywords_attributes: FilterKeywords) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FILTERS_UPDATE_REQUEST }); - - return getClient(getState).filtering.updateFilter(filterId, { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).filtering.updateFilter(filterId, { title, context, filter_action: hide ? 'hide' : 'warn', expires_in, keywords_attributes, }).then(response => { - dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response }); toast.success(messages.added); return response; - }).catch(error => { - dispatch({ type: FILTERS_UPDATE_FAIL, filterId, error }); }); - }; const deleteFilter = (filterId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FILTERS_DELETE_REQUEST }); - return getClient(getState).filtering.deleteFilter(filterId).then(response => { - dispatch({ type: FILTERS_DELETE_SUCCESS, filterId }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).filtering.deleteFilter(filterId).then(response => { toast.success(messages.removed); return response; - }).catch(error => { - dispatch({ type: FILTERS_DELETE_FAIL, filterId, error }); }); - }; + +type FiltersAction = { type: typeof FILTERS_FETCH_SUCCESS; filters: Array }; export { - FILTERS_FETCH_REQUEST, FILTERS_FETCH_SUCCESS, - FILTERS_FETCH_FAIL, - FILTER_FETCH_REQUEST, - FILTER_FETCH_SUCCESS, - FILTER_FETCH_FAIL, - FILTERS_CREATE_REQUEST, - FILTERS_CREATE_SUCCESS, - FILTERS_CREATE_FAIL, - FILTERS_UPDATE_REQUEST, - FILTERS_UPDATE_SUCCESS, - FILTERS_UPDATE_FAIL, - FILTERS_DELETE_REQUEST, - FILTERS_DELETE_SUCCESS, - FILTERS_DELETE_FAIL, fetchFilters, fetchFilter, createFilter, updateFilter, deleteFilter, + type FiltersAction, }; diff --git a/packages/pl-fe/src/actions/groups.ts b/packages/pl-fe/src/actions/groups.ts deleted file mode 100644 index 23686e258..000000000 --- a/packages/pl-fe/src/actions/groups.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { getClient } from '../api'; - -import { importEntities } from './importer'; - -import type { Account, PaginatedResponse } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const GROUP_BLOCKS_FETCH_REQUEST = 'GROUP_BLOCKS_FETCH_REQUEST' as const; -const GROUP_BLOCKS_FETCH_SUCCESS = 'GROUP_BLOCKS_FETCH_SUCCESS' as const; -const GROUP_BLOCKS_FETCH_FAIL = 'GROUP_BLOCKS_FETCH_FAIL' as const; - -const GROUP_UNBLOCK_REQUEST = 'GROUP_UNBLOCK_REQUEST' as const; -const GROUP_UNBLOCK_SUCCESS = 'GROUP_UNBLOCK_SUCCESS' as const; -const GROUP_UNBLOCK_FAIL = 'GROUP_UNBLOCK_FAIL' as const; - -const groupKick = (groupId: string, accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - return getClient(getState).experimental.groups.kickGroupUsers(groupId, [accountId]); - }; - -const fetchGroupBlocks = (groupId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchGroupBlocksRequest(groupId)); - - return getClient(getState).experimental.groups.getGroupBlocks(groupId).then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchGroupBlocksSuccess(groupId, response.items, response.next)); - }).catch(error => { - dispatch(fetchGroupBlocksFail(groupId, error)); - }); - }; - -const fetchGroupBlocksRequest = (groupId: string) => ({ - type: GROUP_BLOCKS_FETCH_REQUEST, - groupId, -}); - -const fetchGroupBlocksSuccess = (groupId: string, accounts: Array, next: (() => Promise>) | null) => ({ - type: GROUP_BLOCKS_FETCH_SUCCESS, - groupId, - accounts, - next, -}); - -const fetchGroupBlocksFail = (groupId: string, error: unknown) => ({ - type: GROUP_BLOCKS_FETCH_FAIL, - groupId, - error, - skipNotFound: true, -}); - -const groupUnblock = (groupId: string, accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(groupUnblockRequest(groupId, accountId)); - - return getClient(getState).experimental.groups.unblockGroupUsers(groupId, [accountId]) - .then(() => dispatch(groupUnblockSuccess(groupId, accountId))) - .catch(err => dispatch(groupUnblockFail(groupId, accountId, err))); - }; - -const groupUnblockRequest = (groupId: string, accountId: string) => ({ - type: GROUP_UNBLOCK_REQUEST, - groupId, - accountId, -}); - -const groupUnblockSuccess = (groupId: string, accountId: string) => ({ - type: GROUP_UNBLOCK_SUCCESS, - groupId, - accountId, -}); - -const groupUnblockFail = (groupId: string, accountId: string, error: unknown) => ({ - type: GROUP_UNBLOCK_FAIL, - groupId, - accountId, - error, -}); - -export { - GROUP_BLOCKS_FETCH_REQUEST, - GROUP_BLOCKS_FETCH_SUCCESS, - GROUP_BLOCKS_FETCH_FAIL, - GROUP_UNBLOCK_REQUEST, - GROUP_UNBLOCK_SUCCESS, - GROUP_UNBLOCK_FAIL, - groupKick, - fetchGroupBlocks, - fetchGroupBlocksRequest, - fetchGroupBlocksSuccess, - fetchGroupBlocksFail, - groupUnblock, - groupUnblockRequest, - groupUnblockSuccess, - groupUnblockFail, -}; diff --git a/packages/pl-fe/src/actions/history.ts b/packages/pl-fe/src/actions/history.ts deleted file mode 100644 index 059a92487..000000000 --- a/packages/pl-fe/src/actions/history.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getClient } from 'pl-fe/api'; - -import { importEntities } from './importer'; - -import type { StatusEdit } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST' as const; -const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS' as const; -const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL' as const; - -const fetchHistory = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const loading = getState().history.getIn([statusId, 'loading']); - - if (loading) return; - - dispatch(fetchHistoryRequest(statusId)); - - return getClient(getState()).statuses.getStatusHistory(statusId).then(data => { - dispatch(importEntities({ accounts: data.map((x) => x.account) })); - dispatch(fetchHistorySuccess(statusId, data)); - }).catch(error => dispatch(fetchHistoryFail(statusId, error))); - }; - -const fetchHistoryRequest = (statusId: string) => ({ - type: HISTORY_FETCH_REQUEST, - statusId, -}); - -const fetchHistorySuccess = (statusId: string, history: Array) => ({ - type: HISTORY_FETCH_SUCCESS, - statusId, - history, -}); - -const fetchHistoryFail = (statusId: string, error: unknown) => ({ - type: HISTORY_FETCH_FAIL, - statusId, - error, -}); - -type HistoryAction = ReturnType | ReturnType | ReturnType; - -export { - HISTORY_FETCH_REQUEST, - HISTORY_FETCH_SUCCESS, - HISTORY_FETCH_FAIL, - fetchHistory, - fetchHistoryRequest, - fetchHistorySuccess, - fetchHistoryFail, - type HistoryAction, -}; diff --git a/packages/pl-fe/src/actions/import-data.ts b/packages/pl-fe/src/actions/import-data.ts index bb83651d7..04359731f 100644 --- a/packages/pl-fe/src/actions/import-data.ts +++ b/packages/pl-fe/src/actions/import-data.ts @@ -5,40 +5,7 @@ import toast from 'pl-fe/toast'; import { getClient } from '../api'; -import type { RootState } from 'pl-fe/store'; - -const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST' as const; -const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS' as const; -const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL' as const; - -const IMPORT_BLOCKS_REQUEST = 'IMPORT_BLOCKS_REQUEST' as const; -const IMPORT_BLOCKS_SUCCESS = 'IMPORT_BLOCKS_SUCCESS' as const; -const IMPORT_BLOCKS_FAIL = 'IMPORT_BLOCKS_FAIL' as const; - -const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST' as const; -const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS' as const; -const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL' as const; - -const IMPORT_ARCHIVE_REQUEST = 'IMPORT_ARCHIVE_REQUEST'; -const IMPORT_ARCHIVE_SUCCESS = 'IMPORT_ARCHIVE_SUCCESS'; -const IMPORT_ARCHIVE_FAIL = 'IMPORT_ARCHIVE_FAIL'; - -type ImportDataActions = { - type: typeof IMPORT_FOLLOWS_REQUEST - | typeof IMPORT_FOLLOWS_SUCCESS - | typeof IMPORT_FOLLOWS_FAIL - | typeof IMPORT_BLOCKS_REQUEST - | typeof IMPORT_BLOCKS_SUCCESS - | typeof IMPORT_BLOCKS_FAIL - | typeof IMPORT_MUTES_REQUEST - | typeof IMPORT_MUTES_SUCCESS - | typeof IMPORT_MUTES_FAIL - | typeof IMPORT_ARCHIVE_REQUEST - | typeof IMPORT_ARCHIVE_SUCCESS - | typeof IMPORT_ARCHIVE_FAIL; - error?: any; - response?: string; -} +import type { AppDispatch, RootState } from 'pl-fe/store'; const messages = defineMessages({ blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' }, @@ -48,68 +15,37 @@ const messages = defineMessages({ }); const importFollows = (list: File | string, overwrite?: boolean) => - (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: IMPORT_FOLLOWS_REQUEST }); - return getClient(getState).settings.importFollows(list, overwrite ? 'overwrite' : 'merge').then(response => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.importFollows(list, overwrite ? 'overwrite' : 'merge').then(response => { toast.success(messages.followersSuccess); - dispatch({ type: IMPORT_FOLLOWS_SUCCESS, response }); - }).catch(error => { - dispatch({ type: IMPORT_FOLLOWS_FAIL, error }); }); - }; const importBlocks = (list: File | string, overwrite?: boolean) => - (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: IMPORT_BLOCKS_REQUEST }); - return getClient(getState).settings.importBlocks(list, overwrite ? 'overwrite' : 'merge').then(response => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.importBlocks(list, overwrite ? 'overwrite' : 'merge').then(response => { toast.success(messages.blocksSuccess); - dispatch({ type: IMPORT_BLOCKS_SUCCESS, response }); - }).catch(error => { - dispatch({ type: IMPORT_BLOCKS_FAIL, error }); }); - }; const importMutes = (list: File | string) => - (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: IMPORT_MUTES_REQUEST }); - return getClient(getState).settings.importMutes(list).then(response => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.importMutes(list).then(response => { toast.success(messages.mutesSuccess); - dispatch({ type: IMPORT_MUTES_SUCCESS, response }); - }).catch(error => { - dispatch({ type: IMPORT_MUTES_FAIL, error }); }); - }; const importArchive = (file: File) => - (dispatch: React.Dispatch, getState: () => RootState) => { - dispatch({ type: IMPORT_ARCHIVE_REQUEST }); + (dispatch: AppDispatch, getState: () => RootState) => { const form = serialize({ file, keep_unlisted: true }, { indices: true }); + return getClient(getState).request('/api/pleroma/archive_import', { method: 'POST', body: form, contentType: '', - }) - .then(response => { - toast.success(messages.archiveSuccess); - dispatch({ type: IMPORT_ARCHIVE_SUCCESS, response: response.json }); - }).catch(error => { - dispatch({ type: IMPORT_ARCHIVE_FAIL, error }); - }); + }).then(() => { + toast.success(messages.archiveSuccess); + }); }; export { - IMPORT_FOLLOWS_REQUEST, - IMPORT_FOLLOWS_SUCCESS, - IMPORT_FOLLOWS_FAIL, - IMPORT_BLOCKS_REQUEST, - IMPORT_BLOCKS_SUCCESS, - IMPORT_BLOCKS_FAIL, - IMPORT_MUTES_REQUEST, - IMPORT_MUTES_SUCCESS, - IMPORT_MUTES_FAIL, - IMPORT_ARCHIVE_REQUEST, - IMPORT_ARCHIVE_SUCCESS, - IMPORT_ARCHIVE_FAIL, importFollows, importBlocks, importMutes, diff --git a/packages/pl-fe/src/actions/instance.ts b/packages/pl-fe/src/actions/instance.ts index 629db73f8..4363b121e 100644 --- a/packages/pl-fe/src/actions/instance.ts +++ b/packages/pl-fe/src/actions/instance.ts @@ -26,15 +26,14 @@ interface InstanceFetchSuccessAction { interface InstanceFetchFailAction { type: typeof INSTANCE_FETCH_FAIL; - error: any; + error: unknown; } const fetchInstance = () => async (dispatch: AppDispatch, getState: () => RootState) => { try { const instance = await getClient(getState).instance.getInstance(); - const action: InstanceFetchSuccessAction = { type: INSTANCE_FETCH_SUCCESS, instance }; - dispatch(action); + dispatch({ type: INSTANCE_FETCH_SUCCESS, instance }); } catch (error) { dispatch({ type: INSTANCE_FETCH_FAIL, error }); } diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index 2909fb1c2..7d2cf8b34 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -6,14 +6,12 @@ import { isLoggedIn } from 'pl-fe/utils/auth'; import { getClient } from '../api'; -import { fetchRelationships } from './accounts'; import { importEntities } from './importer'; -import type { Account, EmojiReaction, PaginatedResponse, Status } from 'pl-api'; +import type { Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const REBLOG_REQUEST = 'REBLOG_REQUEST' as const; -const REBLOG_SUCCESS = 'REBLOG_SUCCESS' as const; const REBLOG_FAIL = 'REBLOG_FAIL' as const; const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST' as const; @@ -21,64 +19,25 @@ const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS' as const; const FAVOURITE_FAIL = 'FAVOURITE_FAIL' as const; const DISLIKE_REQUEST = 'DISLIKE_REQUEST' as const; -const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS' as const; const DISLIKE_FAIL = 'DISLIKE_FAIL' as const; const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST' as const; -const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS' as const; const UNREBLOG_FAIL = 'UNREBLOG_FAIL' as const; const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST' as const; const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS' as const; -const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL' as const; const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST' as const; -const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS' as const; -const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL' as const; -const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST' as const; -const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS' as const; -const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL' as const; - -const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST' as const; -const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS' as const; -const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL' as const; - -const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST' as const; -const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS' as const; -const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL' as const; - -const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST' as const; -const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS' as const; -const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL' as const; - -const PIN_REQUEST = 'PIN_REQUEST' as const; const PIN_SUCCESS = 'PIN_SUCCESS' as const; -const PIN_FAIL = 'PIN_FAIL' as const; -const UNPIN_REQUEST = 'UNPIN_REQUEST' as const; const UNPIN_SUCCESS = 'UNPIN_SUCCESS' as const; -const UNPIN_FAIL = 'UNPIN_FAIL' as const; -const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST' as const; const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS' as const; -const BOOKMARK_FAIL = 'BOOKMARKED_FAIL' as const; -const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST' as const; const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS' as const; -const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL' as const; -const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST' as const; -const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS' as const; -const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL' as const; - -const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS' as const; -const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL' as const; - -const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS' as const; -const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL' as const; - -type AccountListLink = () => Promise>; +const noOp = () => new Promise(f => f(undefined)); const messages = defineMessages({ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, @@ -90,7 +49,7 @@ const messages = defineMessages({ const reblog = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(reblogRequest(status.id)); @@ -98,7 +57,6 @@ const reblog = (status: Pick) => // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper if (response.reblog) dispatch(importEntities({ statuses: [response.reblog] })); - dispatch(reblogSuccess(response)); }).catch(error => { dispatch(reblogFail(status.id, error)); }); @@ -106,37 +64,28 @@ const reblog = (status: Pick) => const unreblog = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(unreblogRequest(status.id)); - return getClient(getState()).statuses.unreblogStatus(status.id).then((status) => { - dispatch(unreblogSuccess(status)); - }).catch(error => { + return getClient(getState()).statuses.unreblogStatus(status.id).catch(error => { dispatch(unreblogFail(status.id, error)); }); }; -const toggleReblog = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.reblogged) { - dispatch(unreblog(status)); - } else { - dispatch(reblog(status)); - } - }; +const toggleReblog = (status: Pick) => { + if (status.reblogged) { + return unreblog(status); + } else { + return reblog(status); + } +}; const reblogRequest = (statusId: string) => ({ type: REBLOG_REQUEST, statusId, }); -const reblogSuccess = (status: Status) => ({ - type: REBLOG_SUCCESS, - status, - statusId: status.id, -}); - const reblogFail = (statusId: string, error: unknown) => ({ type: REBLOG_FAIL, statusId, @@ -148,12 +97,6 @@ const unreblogRequest = (statusId: string) => ({ statusId, }); -const unreblogSuccess = (status: Status) => ({ - type: UNREBLOG_SUCCESS, - status, - statusId: status.id, -}); - const unreblogFail = (statusId: string, error: unknown) => ({ type: UNREBLOG_FAIL, statusId, @@ -162,7 +105,7 @@ const unreblogFail = (statusId: string, error: unknown) => ({ const favourite = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(favouriteRequest(status.id)); @@ -175,25 +118,22 @@ const favourite = (status: Pick) => const unfavourite = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(unfavouriteRequest(status.id)); return getClient(getState()).statuses.unfavouriteStatus(status.id).then((response) => { dispatch(unfavouriteSuccess(response)); - }).catch(error => { - dispatch(unfavouriteFail(status.id, error)); }); }; -const toggleFavourite = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.favourited) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }; +const toggleFavourite = (status: Pick) => { + if (status.favourited) { + return unfavourite(status); + } else { + return favourite(status); + } +}; const favouriteRequest = (statusId: string) => ({ type: FAVOURITE_REQUEST, @@ -223,21 +163,13 @@ const unfavouriteSuccess = (status: Status) => ({ statusId: status.id, }); -const unfavouriteFail = (statusId: string, error: unknown) => ({ - type: UNFAVOURITE_FAIL, - statusId, - error, -}); - const dislike = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; dispatch(dislikeRequest(status.id)); - return getClient(getState).statuses.dislikeStatus(status.id).then((response) => { - dispatch(dislikeSuccess(response)); - }).catch((error) => { + return getClient(getState).statuses.dislikeStatus(status.id).catch((error) => { dispatch(dislikeFail(status.id, error)); }); }; @@ -248,11 +180,7 @@ const undislike = (status: Pick) => dispatch(undislikeRequest(status.id)); - return getClient(getState).statuses.undislikeStatus(status.id).then((response) => { - dispatch(undislikeSuccess(response)); - }).catch(error => { - dispatch(undislikeFail(status.id, error)); - }); + return getClient(getState).statuses.undislikeStatus(status.id); }; const toggleDislike = (status: Pick) => @@ -269,12 +197,6 @@ const dislikeRequest = (statusId: string) => ({ statusId, }); -const dislikeSuccess = (status: Status) => ({ - type: DISLIKE_SUCCESS, - status, - statusId: status.id, -}); - const dislikeFail = (statusId: string, error: unknown) => ({ type: DISLIKE_FAIL, statusId, @@ -286,26 +208,12 @@ const undislikeRequest = (statusId: string) => ({ statusId, }); -const undislikeSuccess = (status: Status) => ({ - type: UNDISLIKE_SUCCESS, - status, - statusId: status.id, -}); - -const undislikeFail = (statusId: string, error: unknown) => ({ - type: UNDISLIKE_FAIL, - statusId, - error, -}); - const bookmark = (status: Pick, folderId?: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const features = state.auth.client.features; - dispatch(bookmarkRequest(status.id)); - return getClient(getState()).statuses.bookmarkStatus(status.id, folderId).then((response) => { dispatch(importEntities({ statuses: [response] })); dispatch(bookmarkSuccess(response)); @@ -325,23 +233,16 @@ const bookmark = (status: Pick, folderId?: string) => } toast.success(typeof folderId === 'string' ? messages.folderChanged : messages.bookmarkAdded, opts); - }).catch((error) => { - dispatch(bookmarkFail(status.id, error)); }); }; const unbookmark = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(unbookmarkRequest(status.id)); - - return getClient(getState()).statuses.unbookmarkStatus(status.id).then(response => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).statuses.unbookmarkStatus(status.id).then(response => { dispatch(importEntities({ statuses: [response] })); dispatch(unbookmarkSuccess(response)); toast.success(messages.bookmarkRemoved); - }).catch(error => { - dispatch(unbookmarkFail(status.id, error)); }); - }; const toggleBookmark = (status: Pick) => (dispatch: AppDispatch) => { @@ -352,229 +253,29 @@ const toggleBookmark = (status: Pick) => } }; -const bookmarkRequest = (statusId: string) => ({ - type: BOOKMARK_REQUEST, - statusId, -}); - const bookmarkSuccess = (status: Status) => ({ type: BOOKMARK_SUCCESS, status, statusId: status.id, }); -const bookmarkFail = (statusId: string, error: unknown) => ({ - type: BOOKMARK_FAIL, - statusId, - error, -}); - -const unbookmarkRequest = (statusId: string) => ({ - type: UNBOOKMARK_REQUEST, - statusId, -}); - const unbookmarkSuccess = (status: Status) => ({ type: UNBOOKMARK_SUCCESS, status, statusId: status.id, }); -const unbookmarkFail = (statusId: string, error: unknown) => ({ - type: UNBOOKMARK_FAIL, - statusId, - error, -}); - -const fetchReblogs = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchReblogsRequest(statusId)); - - return getClient(getState()).statuses.getRebloggedBy(statusId).then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchRelationships(response.items.map((item) => item.id))); - dispatch(fetchReblogsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(fetchReblogsFail(statusId, error)); - }); - }; - -const fetchReblogsRequest = (statusId: string) => ({ - type: REBLOGS_FETCH_REQUEST, - statusId, -}); - -const fetchReblogsSuccess = (statusId: string, accounts: Array, next: AccountListLink | null) => ({ - type: REBLOGS_FETCH_SUCCESS, - statusId, - accounts, - next, -}); - -const fetchReblogsFail = (statusId: string, error: unknown) => ({ - type: REBLOGS_FETCH_FAIL, - statusId, - error, -}); - -const expandReblogs = (statusId: string, next: AccountListLink) => - (dispatch: AppDispatch, getState: () => RootState) => { - next().then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchRelationships(response.items.map((item) => item.id))); - dispatch(expandReblogsSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(expandReblogsFail(statusId, error)); - }); - }; - -const expandReblogsSuccess = (statusId: string, accounts: Array, next: AccountListLink | null) => ({ - type: REBLOGS_EXPAND_SUCCESS, - statusId, - accounts, - next, -}); - -const expandReblogsFail = (statusId: string, error: unknown) => ({ - type: REBLOGS_EXPAND_FAIL, - statusId, - error, -}); - -const fetchFavourites = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchFavouritesRequest(statusId)); - - return getClient(getState()).statuses.getFavouritedBy(statusId).then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchRelationships(response.items.map((item) => item.id))); - dispatch(fetchFavouritesSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(fetchFavouritesFail(statusId, error)); - }); - }; - -const fetchFavouritesRequest = (statusId: string) => ({ - type: FAVOURITES_FETCH_REQUEST, - statusId, -}); - -const fetchFavouritesSuccess = (statusId: string, accounts: Array, next: AccountListLink | null) => ({ - type: FAVOURITES_FETCH_SUCCESS, - statusId, - accounts, - next, -}); - -const fetchFavouritesFail = (statusId: string, error: unknown) => ({ - type: FAVOURITES_FETCH_FAIL, - statusId, - error, -}); - -const expandFavourites = (statusId: string, next: AccountListLink) => - (dispatch: AppDispatch) => { - next().then(response => { - dispatch(importEntities({ accounts: response.items })); - dispatch(fetchRelationships(response.items.map((item) => item.id))); - dispatch(expandFavouritesSuccess(statusId, response.items, response.next)); - }).catch(error => { - dispatch(expandFavouritesFail(statusId, error)); - }); - }; - -const expandFavouritesSuccess = (statusId: string, accounts: Array, next: AccountListLink | null) => ({ - type: FAVOURITES_EXPAND_SUCCESS, - statusId, - accounts, - next, -}); - -const expandFavouritesFail = (statusId: string, error: unknown) => ({ - type: FAVOURITES_EXPAND_FAIL, - statusId, - error, -}); - -const fetchDislikes = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchDislikesRequest(statusId)); - - return getClient(getState).statuses.getDislikedBy(statusId).then(response => { - dispatch(importEntities({ accounts: response })); - dispatch(fetchRelationships(response.map((item) => item.id))); - dispatch(fetchDislikesSuccess(statusId, response)); - }).catch(error => { - dispatch(fetchDislikesFail(statusId, error)); - }); - }; - -const fetchDislikesRequest = (statusId: string) => ({ - type: DISLIKES_FETCH_REQUEST, - statusId, -}); - -const fetchDislikesSuccess = (statusId: string, accounts: Array) => ({ - type: DISLIKES_FETCH_SUCCESS, - statusId, - accounts, -}); - -const fetchDislikesFail = (statusId: string, error: unknown) => ({ - type: DISLIKES_FETCH_FAIL, - statusId, - error, -}); - -const fetchReactions = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchReactionsRequest(statusId)); - - return getClient(getState).statuses.getStatusReactions(statusId).then(response => { - dispatch(importEntities({ accounts: (response).map(({ accounts }) => accounts).flat() })); - dispatch(fetchReactionsSuccess(statusId, response)); - }).catch(error => { - dispatch(fetchReactionsFail(statusId, error)); - }); - }; - -const fetchReactionsRequest = (statusId: string) => ({ - type: REACTIONS_FETCH_REQUEST, - statusId, -}); - -const fetchReactionsSuccess = (statusId: string, reactions: EmojiReaction[]) => ({ - type: REACTIONS_FETCH_SUCCESS, - statusId, - reactions, -}); - -const fetchReactionsFail = (statusId: string, error: unknown) => ({ - type: REACTIONS_FETCH_FAIL, - statusId, - error, -}); - const pin = (status: Pick, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(pinRequest(status.id, accountId)); - return getClient(getState()).statuses.pinStatus(status.id).then(response => { dispatch(importEntities({ statuses: [response] })); dispatch(pinSuccess(response, accountId)); }).catch(error => { - dispatch(pinFail(status.id, error, accountId)); }); }; -const pinRequest = (statusId: string, accountId: string) => ({ - type: PIN_REQUEST, - statusId, - accountId, -}); - const pinSuccess = (status: Status, accountId: string) => ({ type: PIN_SUCCESS, status, @@ -582,24 +283,13 @@ const pinSuccess = (status: Status, accountId: string) => ({ accountId, }); -const pinFail = (statusId: string, error: unknown, accountId: string) => ({ - type: PIN_FAIL, - statusId, - error, - accountId, -}); - const unpin = (status: Pick, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(unpinRequest(status.id, accountId)); - return getClient(getState()).statuses.unpinStatus(status.id).then(response => { dispatch(importEntities({ statuses: [response] })); dispatch(unpinSuccess(response, accountId)); - }).catch(error => { - dispatch(unpinFail(status.id, error, accountId)); }); }; @@ -616,12 +306,6 @@ const togglePin = (status: Pick) => } }; -const unpinRequest = (statusId: string, accountId: string) => ({ - type: UNPIN_REQUEST, - statusId, - accountId, -}); - const unpinSuccess = (status: Status, accountId: string) => ({ type: UNPIN_SUCCESS, status, @@ -629,210 +313,55 @@ const unpinSuccess = (status: Status, accountId: string) => ({ accountId, }); -const unpinFail = (statusId: string, error: unknown, accountId: string) => ({ - type: UNPIN_FAIL, - statusId, - error, - accountId, -}); - const remoteInteraction = (ap_id: string, profile: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(remoteInteractionRequest(ap_id, profile)); - - return getClient(getState).accounts.remoteInteraction(ap_id, profile).then((data) => { - dispatch(remoteInteractionSuccess(ap_id, profile, data.url)); - - return data.url; - }).catch(error => { - dispatch(remoteInteractionFail(ap_id, profile, error)); - throw error; - }); - }; - -const remoteInteractionRequest = (ap_id: string, profile: string) => ({ - type: REMOTE_INTERACTION_REQUEST, - ap_id, - profile, -}); - -const remoteInteractionSuccess = (ap_id: string, profile: string, url: string) => ({ - type: REMOTE_INTERACTION_SUCCESS, - ap_id, - profile, - url, -}); - -const remoteInteractionFail = (ap_id: string, profile: string, error: unknown) => ({ - type: REMOTE_INTERACTION_FAIL, - ap_id, - profile, - error, -}); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).accounts.remoteInteraction(ap_id, profile).then((data) => data.url); type InteractionsAction = - ReturnType - | ReturnType + | ReturnType | ReturnType | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType | ReturnType | ReturnType | ReturnType - | ReturnType | ReturnType - | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; export { REBLOG_REQUEST, - REBLOG_SUCCESS, REBLOG_FAIL, FAVOURITE_REQUEST, FAVOURITE_SUCCESS, FAVOURITE_FAIL, DISLIKE_REQUEST, - DISLIKE_SUCCESS, DISLIKE_FAIL, UNREBLOG_REQUEST, - UNREBLOG_SUCCESS, UNREBLOG_FAIL, UNFAVOURITE_REQUEST, UNFAVOURITE_SUCCESS, - UNFAVOURITE_FAIL, UNDISLIKE_REQUEST, - UNDISLIKE_SUCCESS, - UNDISLIKE_FAIL, - REBLOGS_FETCH_REQUEST, - REBLOGS_FETCH_SUCCESS, - REBLOGS_FETCH_FAIL, - FAVOURITES_FETCH_REQUEST, - FAVOURITES_FETCH_SUCCESS, - FAVOURITES_FETCH_FAIL, - DISLIKES_FETCH_REQUEST, - DISLIKES_FETCH_SUCCESS, - DISLIKES_FETCH_FAIL, - REACTIONS_FETCH_REQUEST, - REACTIONS_FETCH_SUCCESS, - REACTIONS_FETCH_FAIL, - PIN_REQUEST, PIN_SUCCESS, - PIN_FAIL, - UNPIN_REQUEST, UNPIN_SUCCESS, - UNPIN_FAIL, - BOOKMARK_REQUEST, BOOKMARK_SUCCESS, - BOOKMARK_FAIL, - UNBOOKMARK_REQUEST, UNBOOKMARK_SUCCESS, - UNBOOKMARK_FAIL, - REMOTE_INTERACTION_REQUEST, - REMOTE_INTERACTION_SUCCESS, - REMOTE_INTERACTION_FAIL, - FAVOURITES_EXPAND_SUCCESS, - FAVOURITES_EXPAND_FAIL, - REBLOGS_EXPAND_SUCCESS, - REBLOGS_EXPAND_FAIL, reblog, unreblog, toggleReblog, - reblogRequest, - reblogSuccess, - reblogFail, - unreblogRequest, - unreblogSuccess, - unreblogFail, favourite, unfavourite, toggleFavourite, - favouriteRequest, - favouriteSuccess, - favouriteFail, - unfavouriteRequest, - unfavouriteSuccess, - unfavouriteFail, - dislike, - undislike, toggleDislike, - dislikeRequest, - dislikeSuccess, - dislikeFail, - undislikeRequest, - undislikeSuccess, - undislikeFail, bookmark, - unbookmark, toggleBookmark, - bookmarkRequest, - bookmarkSuccess, - bookmarkFail, - unbookmarkRequest, - unbookmarkSuccess, - unbookmarkFail, - fetchReblogs, - fetchReblogsRequest, - fetchReblogsSuccess, - fetchReblogsFail, - expandReblogs, - fetchFavourites, - fetchFavouritesRequest, - fetchFavouritesSuccess, - fetchFavouritesFail, - expandFavourites, - fetchDislikes, - fetchDislikesRequest, - fetchDislikesSuccess, - fetchDislikesFail, - fetchReactions, - fetchReactionsRequest, - fetchReactionsSuccess, - fetchReactionsFail, - pin, - pinRequest, - pinSuccess, - pinFail, - unpin, - unpinRequest, - unpinSuccess, - unpinFail, togglePin, remoteInteraction, - remoteInteractionRequest, - remoteInteractionSuccess, - remoteInteractionFail, type InteractionsAction, }; diff --git a/packages/pl-fe/src/actions/languages.ts b/packages/pl-fe/src/actions/languages.ts deleted file mode 100644 index fe26a5345..000000000 --- a/packages/pl-fe/src/actions/languages.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { saveSettings } from './settings'; - -import type { AppDispatch } from 'pl-fe/store'; - -const LANGUAGE_USE = 'LANGUAGE_USE' as const; - -const rememberLanguageUse = (language: string) => (dispatch: AppDispatch) => { - dispatch({ - type: LANGUAGE_USE, - language, - }); - - dispatch(saveSettings()); -}; - -export { LANGUAGE_USE, rememberLanguageUse }; diff --git a/packages/pl-fe/src/actions/lists.ts b/packages/pl-fe/src/actions/lists.ts index 5c8625730..8a64992a6 100644 --- a/packages/pl-fe/src/actions/lists.ts +++ b/packages/pl-fe/src/actions/lists.ts @@ -9,13 +9,10 @@ import { importEntities } from './importer'; import type { Account, List, PaginatedResponse } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST' as const; const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS' as const; const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL' as const; -const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST' as const; const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS' as const; -const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL' as const; const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE' as const; const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET' as const; @@ -29,9 +26,7 @@ const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST' as const; const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS' as const; const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL' as const; -const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST' as const; const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS' as const; -const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL' as const; const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST' as const; const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS' as const; @@ -41,13 +36,9 @@ const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE' as const const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY' as const; const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR' as const; -const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST' as const; const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS' as const; -const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL' as const; -const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST' as const; const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS' as const; -const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL' as const; const LIST_ADDER_RESET = 'LIST_ADDER_RESET' as const; const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP' as const; @@ -59,22 +50,15 @@ const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL' as const; const fetchList = (listId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - if (getState().lists.get(listId)) { + if (getState().lists[listId]) { return; } - dispatch(fetchListRequest(listId)); - return getClient(getState()).lists.getList(listId) .then((data) => dispatch(fetchListSuccess(data))) .catch(err => dispatch(fetchListFail(listId, err))); }; -const fetchListRequest = (listId: string) => ({ - type: LIST_FETCH_REQUEST, - listId, -}); - const fetchListSuccess = (list: List) => ({ type: LIST_FETCH_SUCCESS, list, @@ -89,27 +73,15 @@ const fetchListFail = (listId: string, error: unknown) => ({ const fetchLists = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(fetchListsRequest()); - return getClient(getState()).lists.getLists() - .then((data) => dispatch(fetchListsSuccess(data))) - .catch(err => dispatch(fetchListsFail(err))); + .then((data) => dispatch(fetchListsSuccess(data))); }; -const fetchListsRequest = () => ({ - type: LISTS_FETCH_REQUEST, -}); - const fetchListsSuccess = (lists: Array) => ({ type: LISTS_FETCH_SUCCESS, lists, }); -const fetchListsFail = (error: unknown) => ({ - type: LISTS_FETCH_FAIL, - error, -}); - const submitListEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { const listId = getState().listEditor.listId!; const title = getState().listEditor.title; @@ -121,10 +93,18 @@ const submitListEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getS } }; +interface ListEditorSetupAction { + type: typeof LIST_EDITOR_SETUP; + list: List; +} + const setupListEditor = (listId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ + const list = getState().lists[listId]; + if (!list) return; + + dispatch({ type: LIST_EDITOR_SETUP, - list: getState().lists.get(String(listId)), + list, }); dispatch(fetchListAccounts(listId)); @@ -200,29 +180,15 @@ const resetListEditor = () => ({ const deleteList = (listId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(deleteListRequest(listId)); - return getClient(getState()).lists.deleteList(listId) - .then(() => dispatch(deleteListSuccess(listId))) - .catch(err => dispatch(deleteListFail(listId, err))); + .then(() => dispatch(deleteListSuccess(listId))); }; -const deleteListRequest = (listId: string) => ({ - type: LIST_DELETE_REQUEST, - listId, -}); - const deleteListSuccess = (listId: string) => ({ type: LIST_DELETE_SUCCESS, listId, }); -const deleteListFail = (listId: string, error: unknown) => ({ - type: LIST_DELETE_FAIL, - listId, - error, -}); - const fetchListAccounts = (listId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -283,32 +249,16 @@ const addToListEditor = (accountId: string) => (dispatch: AppDispatch, getState: const addToList = (listId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(addToListRequest(listId, accountId)); - return getClient(getState()).lists.addListAccounts(listId, [accountId]) - .then(() => dispatch(addToListSuccess(listId, accountId))) - .catch(err => dispatch(addToListFail(listId, accountId, err))); + .then(() => dispatch(addToListSuccess(listId, accountId))); }; -const addToListRequest = (listId: string, accountId: string) => ({ - type: LIST_EDITOR_ADD_REQUEST, - listId, - accountId, -}); - const addToListSuccess = (listId: string, accountId: string) => ({ type: LIST_EDITOR_ADD_SUCCESS, listId, accountId, }); -const addToListFail = (listId: string, accountId: string, error: any) => ({ - type: LIST_EDITOR_ADD_FAIL, - listId, - accountId, - error, -}); - const removeFromListEditor = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(removeFromList(getState().listEditor.listId!, accountId)); }; @@ -316,40 +266,33 @@ const removeFromListEditor = (accountId: string) => (dispatch: AppDispatch, getS const removeFromList = (listId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(removeFromListRequest(listId, accountId)); - return getClient(getState()).lists.deleteListAccounts(listId, [accountId]) - .then(() => dispatch(removeFromListSuccess(listId, accountId))) - .catch(err => dispatch(removeFromListFail(listId, accountId, err))); + .then(() => dispatch(removeFromListSuccess(listId, accountId))); }; -const removeFromListRequest = (listId: string, accountId: string) => ({ - type: LIST_EDITOR_REMOVE_REQUEST, - listId, - accountId, -}); - const removeFromListSuccess = (listId: string, accountId: string) => ({ type: LIST_EDITOR_REMOVE_SUCCESS, listId, accountId, }); -const removeFromListFail = (listId: string, accountId: string, error: unknown) => ({ - type: LIST_EDITOR_REMOVE_FAIL, - listId, - accountId, - error, -}); const resetListAdder = () => ({ type: LIST_ADDER_RESET, }); +interface ListAdderSetupAction { + type: typeof LIST_ADDER_SETUP; + account: Account; +} + const setupListAdder = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ + const account = selectAccount(getState(), accountId); + if (!account) return; + + dispatch({ type: LIST_ADDER_SETUP, - account: selectAccount(getState(), accountId), + account, }); dispatch(fetchLists()); dispatch(fetchAccountLists(accountId)); @@ -390,13 +333,38 @@ const removeFromListAdder = (listId: string) => (dispatch: AppDispatch, getState dispatch(removeFromList(listId, getState().listAdder.accountId!)); }; +type ListsAction = + | ReturnType + | ReturnType + | ReturnType + | ListEditorSetupAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ListAdderSetupAction + | ReturnType + | ReturnType + | ReturnType; + export { - LIST_FETCH_REQUEST, LIST_FETCH_SUCCESS, LIST_FETCH_FAIL, - LISTS_FETCH_REQUEST, LISTS_FETCH_SUCCESS, - LISTS_FETCH_FAIL, LIST_EDITOR_TITLE_CHANGE, LIST_EDITOR_RESET, LIST_EDITOR_SETUP, @@ -406,74 +374,35 @@ export { LIST_UPDATE_REQUEST, LIST_UPDATE_SUCCESS, LIST_UPDATE_FAIL, - LIST_DELETE_REQUEST, LIST_DELETE_SUCCESS, - LIST_DELETE_FAIL, LIST_ACCOUNTS_FETCH_REQUEST, LIST_ACCOUNTS_FETCH_SUCCESS, LIST_ACCOUNTS_FETCH_FAIL, LIST_EDITOR_SUGGESTIONS_CHANGE, LIST_EDITOR_SUGGESTIONS_READY, LIST_EDITOR_SUGGESTIONS_CLEAR, - LIST_EDITOR_ADD_REQUEST, LIST_EDITOR_ADD_SUCCESS, - LIST_EDITOR_ADD_FAIL, - LIST_EDITOR_REMOVE_REQUEST, LIST_EDITOR_REMOVE_SUCCESS, - LIST_EDITOR_REMOVE_FAIL, LIST_ADDER_RESET, LIST_ADDER_SETUP, LIST_ADDER_LISTS_FETCH_REQUEST, LIST_ADDER_LISTS_FETCH_SUCCESS, LIST_ADDER_LISTS_FETCH_FAIL, fetchList, - fetchListRequest, - fetchListSuccess, - fetchListFail, fetchLists, - fetchListsRequest, - fetchListsSuccess, - fetchListsFail, submitListEditor, setupListEditor, changeListEditorTitle, - createList, - createListRequest, - createListSuccess, - createListFail, - updateList, - updateListRequest, - updateListSuccess, - updateListFail, resetListEditor, deleteList, - deleteListRequest, - deleteListSuccess, - deleteListFail, - fetchListAccounts, - fetchListAccountsRequest, - fetchListAccountsSuccess, - fetchListAccountsFail, fetchListSuggestions, - fetchListSuggestionsReady, clearListSuggestions, changeListSuggestions, addToListEditor, - addToList, - addToListRequest, - addToListSuccess, - addToListFail, removeFromListEditor, - removeFromList, - removeFromListRequest, - removeFromListSuccess, - removeFromListFail, resetListAdder, setupListAdder, - fetchAccountLists, - fetchAccountListsRequest, - fetchAccountListsSuccess, - fetchAccountListsFail, addToListAdder, removeFromListAdder, + type ListsAction, }; diff --git a/packages/pl-fe/src/actions/markers.ts b/packages/pl-fe/src/actions/markers.ts index 743a54744..ce16d6a35 100644 --- a/packages/pl-fe/src/actions/markers.ts +++ b/packages/pl-fe/src/actions/markers.ts @@ -1,43 +1,38 @@ import { getClient } from '../api'; -import type { SaveMarkersParams } from 'pl-api'; +import type { Markers, SaveMarkersParams } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const MARKER_FETCH_REQUEST = 'MARKER_FETCH_REQUEST' as const; const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS' as const; -const MARKER_FETCH_FAIL = 'MARKER_FETCH_FAIL' as const; -const MARKER_SAVE_REQUEST = 'MARKER_SAVE_REQUEST' as const; const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS' as const; -const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL' as const; const fetchMarker = (timeline: Array) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MARKER_FETCH_REQUEST }); - return getClient(getState).timelines.getMarkers(timeline).then((marker) => { - dispatch({ type: MARKER_FETCH_SUCCESS, marker }); - }).catch(error => { - dispatch({ type: MARKER_FETCH_FAIL, error }); + (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) => { - dispatch({ type: MARKER_SAVE_REQUEST, marker }); - return getClient(getState).timelines.saveMarkers(marker).then((marker) => { - dispatch({ type: MARKER_SAVE_SUCCESS, marker }); - }).catch(error => { - dispatch({ type: MARKER_SAVE_FAIL, error }); + (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_REQUEST, MARKER_FETCH_SUCCESS, - MARKER_FETCH_FAIL, - MARKER_SAVE_REQUEST, MARKER_SAVE_SUCCESS, - MARKER_SAVE_FAIL, fetchMarker, saveMarker, + type MarkersAction, }; diff --git a/packages/pl-fe/src/actions/me.ts b/packages/pl-fe/src/actions/me.ts index 60535df7e..a90874cfc 100644 --- a/packages/pl-fe/src/actions/me.ts +++ b/packages/pl-fe/src/actions/me.ts @@ -13,14 +13,11 @@ import { FE_NAME } from './settings'; import type { CredentialAccount, UpdateCredentialsParams } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST' as const; const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS' as const; const ME_FETCH_FAIL = 'ME_FETCH_FAIL' as const; const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const; -const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST' as const; const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const; -const ME_PATCH_FAIL = 'ME_PATCH_FAIL' as const; const noOp = () => new Promise(f => f(undefined)); @@ -36,9 +33,13 @@ const getMeUrl = (state: RootState) => { const getMeToken = (state: RootState) => { // Fallback for upgrading IDs to URLs const accountUrl = getMeUrl(state) || state.auth.me; - return state.auth.users.get(accountUrl!)?.access_token; + return state.auth.users[accountUrl!]?.access_token; }; +interface MeFetchSkipAction { + type: typeof ME_FETCH_SKIP; +} + const fetchMe = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -46,11 +47,10 @@ const fetchMe = () => const accountUrl = getMeUrl(state); if (!token) { - dispatch({ type: ME_FETCH_SKIP }); + dispatch({ type: ME_FETCH_SKIP }); return noOp(); } - dispatch(fetchMeRequest()); return dispatch(loadCredentials(token, accountUrl!)) .catch(error => dispatch(fetchMeFail(error))); }; @@ -77,22 +77,12 @@ const persistAuthAccount = (account: CredentialAccount, params: Record - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(patchMeRequest()); - - return getClient(getState).settings.updateCredentials(params) + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.updateCredentials(params) .then(response => { persistAuthAccount(response, params); dispatch(patchMeSuccess(response)); - }).catch(error => { - dispatch(patchMeFail(error)); - throw error; }); - }; - -const fetchMeRequest = () => ({ - type: ME_FETCH_REQUEST, -}); const fetchMeSuccess = (account: CredentialAccount) => { setSentryAccount(account); @@ -111,10 +101,6 @@ const fetchMeFail = (error: unknown) => ({ skipAlert: true, }); -const patchMeRequest = () => ({ - type: ME_PATCH_REQUEST, -}); - interface MePatchSuccessAction { type: typeof ME_PATCH_SUCCESS; me: CredentialAccount; @@ -122,44 +108,28 @@ interface MePatchSuccessAction { const patchMeSuccess = (me: CredentialAccount) => (dispatch: AppDispatch) => { - const action: MePatchSuccessAction = { + dispatch(importEntities({ accounts: [me] })); + dispatch({ type: ME_PATCH_SUCCESS, me, - }; - - dispatch(importEntities({ accounts: [me] })); - dispatch(action); + }); }; -const patchMeFail = (error: unknown) => ({ - type: ME_PATCH_FAIL, - error, - skipAlert: true, -}); - type MeAction = - | ReturnType | ReturnType | ReturnType - | ReturnType + | MeFetchSkipAction | MePatchSuccessAction - | ReturnType; export { - ME_FETCH_REQUEST, ME_FETCH_SUCCESS, ME_FETCH_FAIL, ME_FETCH_SKIP, - ME_PATCH_REQUEST, ME_PATCH_SUCCESS, - ME_PATCH_FAIL, fetchMe, patchMe, - fetchMeRequest, fetchMeSuccess, fetchMeFail, - patchMeRequest, patchMeSuccess, - patchMeFail, type MeAction, }; diff --git a/packages/pl-fe/src/actions/media.ts b/packages/pl-fe/src/actions/media.ts index 7bfd4b293..0879dfe6f 100644 --- a/packages/pl-fe/src/actions/media.ts +++ b/packages/pl-fe/src/actions/media.ts @@ -16,10 +16,10 @@ const messages = defineMessages({ exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' }, }); -const noOp = (e: any) => {}; +const noOp = () => {}; const updateMedia = (mediaId: string, params: Record) => - (dispatch: any, getState: () => RootState) => + (_dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).media.updateMedia(mediaId, params); const uploadMedia = (body: UploadMediaParams, onUploadProgress: (e: ProgressEvent) => void = noOp) => diff --git a/packages/pl-fe/src/actions/mfa.ts b/packages/pl-fe/src/actions/mfa.ts index dbdad8d8e..6cb33a273 100644 --- a/packages/pl-fe/src/actions/mfa.ts +++ b/packages/pl-fe/src/actions/mfa.ts @@ -1,104 +1,55 @@ import { getClient } from '../api'; +import type { PlApiClient } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const MFA_FETCH_REQUEST = 'MFA_FETCH_REQUEST' as const; const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS' as const; -const MFA_FETCH_FAIL = 'MFA_FETCH_FAIL' as const; -const MFA_BACKUP_CODES_FETCH_REQUEST = 'MFA_BACKUP_CODES_FETCH_REQUEST' as const; -const MFA_BACKUP_CODES_FETCH_SUCCESS = 'MFA_BACKUP_CODES_FETCH_SUCCESS' as const; -const MFA_BACKUP_CODES_FETCH_FAIL = 'MFA_BACKUP_CODES_FETCH_FAIL' as const; - -const MFA_SETUP_REQUEST = 'MFA_SETUP_REQUEST' as const; -const MFA_SETUP_SUCCESS = 'MFA_SETUP_SUCCESS' as const; -const MFA_SETUP_FAIL = 'MFA_SETUP_FAIL' as const; - -const MFA_CONFIRM_REQUEST = 'MFA_CONFIRM_REQUEST' as const; const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS' as const; -const MFA_CONFIRM_FAIL = 'MFA_CONFIRM_FAIL' as const; -const MFA_DISABLE_REQUEST = 'MFA_DISABLE_REQUEST' as const; const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS' as const; -const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL' as const; const fetchMfa = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MFA_FETCH_REQUEST }); - return getClient(getState).settings.mfa.getMfaSettings().then((data) => { - dispatch({ type: MFA_FETCH_SUCCESS, data }); - }).catch(() => { - dispatch({ type: MFA_FETCH_FAIL }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.mfa.getMfaSettings().then((data) => { + dispatch({ type: MFA_FETCH_SUCCESS, data }); }); - }; const fetchBackupCodes = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST }); - return getClient(getState).settings.mfa.getMfaBackupCodes().then((data) => { - dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data }); - return data; - }).catch((error: unknown) => { - dispatch({ type: MFA_BACKUP_CODES_FETCH_FAIL }); - throw error; - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.mfa.getMfaBackupCodes(); const setupMfa = (method: 'totp') => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MFA_SETUP_REQUEST, method }); - return getClient(getState).settings.mfa.getMfaSetup(method).then((data) => { - dispatch({ type: MFA_SETUP_SUCCESS, data }); - return data; - }).catch((error: unknown) => { - dispatch({ type: MFA_SETUP_FAIL }); - throw error; - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.mfa.getMfaSetup(method); const confirmMfa = (method: 'totp', code: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MFA_CONFIRM_REQUEST, method, code }); - return getClient(getState).settings.mfa.confirmMfaSetup(method, code, password).then((data) => { - dispatch({ type: MFA_CONFIRM_SUCCESS, method, code }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.mfa.confirmMfaSetup(method, code, password).then((data) => { + dispatch({ type: MFA_CONFIRM_SUCCESS, method, code }); return data; - }).catch((error: unknown) => { - dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true }); - throw error; }); - }; const disableMfa = (method: 'totp', password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MFA_DISABLE_REQUEST, method }); - return getClient(getState).settings.mfa.disableMfa(method, password).then((data) => { - dispatch({ type: MFA_DISABLE_SUCCESS, method }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.mfa.disableMfa(method, password).then((data) => { + dispatch({ type: MFA_DISABLE_SUCCESS, method }); return data; - }).catch((error: unknown) => { - dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true }); - throw error; }); - }; + +type MfaAction = + | { type: typeof MFA_FETCH_SUCCESS; data: Awaited)['settings']['mfa']['getMfaSettings']>> } + | { type: typeof MFA_CONFIRM_SUCCESS; method: 'totp'; code: string } + | { type: typeof MFA_DISABLE_SUCCESS; method: 'totp' } export { - MFA_FETCH_REQUEST, MFA_FETCH_SUCCESS, - MFA_FETCH_FAIL, - MFA_BACKUP_CODES_FETCH_REQUEST, - MFA_BACKUP_CODES_FETCH_SUCCESS, - MFA_BACKUP_CODES_FETCH_FAIL, - MFA_SETUP_REQUEST, - MFA_SETUP_SUCCESS, - MFA_SETUP_FAIL, - MFA_CONFIRM_REQUEST, MFA_CONFIRM_SUCCESS, - MFA_CONFIRM_FAIL, - MFA_DISABLE_REQUEST, MFA_DISABLE_SUCCESS, - MFA_DISABLE_FAIL, fetchMfa, fetchBackupCodes, setupMfa, confirmMfa, disableMfa, + type MfaAction, }; diff --git a/packages/pl-fe/src/actions/moderation.tsx b/packages/pl-fe/src/actions/moderation.tsx index ddb490f2f..3ef1e0b91 100644 --- a/packages/pl-fe/src/actions/moderation.tsx +++ b/packages/pl-fe/src/actions/moderation.tsx @@ -114,7 +114,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const acct = state.statuses.get(statusId)!.account.acct; + const acct = state.statuses[statusId]!.account.acct; useModalsStore.getState().openModal('CONFIRM', { heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading), @@ -133,7 +133,7 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const acct = state.statuses.get(statusId)!.account.acct; + const acct = state.statuses[statusId]!.account.acct; useModalsStore.getState().openModal('CONFIRM', { heading: intl.formatMessage(messages.deleteStatusHeading), diff --git a/packages/pl-fe/src/actions/mrf.ts b/packages/pl-fe/src/actions/mrf.ts index b5e4d2da6..6b16312d6 100644 --- a/packages/pl-fe/src/actions/mrf.ts +++ b/packages/pl-fe/src/actions/mrf.ts @@ -26,7 +26,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: const updateMrf = (host: string, restrictions: Record) => (dispatch: AppDispatch, getState: () => RootState) => dispatch(fetchConfig()).then(() => { - const configs = getState().admin.get('configs'); + const configs = getState().admin.configs; const simplePolicy = ConfigDB.toSimplePolicy(configs); const merged = simplePolicyMerge(simplePolicy, host, restrictions); const config = ConfigDB.fromSimplePolicy(merged); diff --git a/packages/pl-fe/src/actions/mutes.ts b/packages/pl-fe/src/actions/mutes.ts deleted file mode 100644 index 559045beb..000000000 --- a/packages/pl-fe/src/actions/mutes.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useModalsStore } from 'pl-fe/stores/modals'; - -import type { Account } from 'pl-fe/normalizers/account'; -import type { AppDispatch } from 'pl-fe/store'; - -const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; -const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; -const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; - -const initMuteModal = (account: Account) => - (dispatch: AppDispatch) => { - dispatch({ - type: MUTES_INIT_MODAL, - account, - }); - - useModalsStore.getState().openModal('MUTE'); - }; - -const toggleHideNotifications = () => - (dispatch: AppDispatch) => { - dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); - }; - -const changeMuteDuration = (duration: number) => - (dispatch: AppDispatch) => { - dispatch({ - type: MUTES_CHANGE_DURATION, - duration, - }); - }; - -export { - MUTES_INIT_MODAL, - MUTES_TOGGLE_HIDE_NOTIFICATIONS, - MUTES_CHANGE_DURATION, - initMuteModal, - toggleHideNotifications, - changeMuteDuration, -}; diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index e41c7a091..5f93b8d98 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -4,7 +4,8 @@ import { defineMessages } from 'react-intl'; import { getClient } from 'pl-fe/api'; import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification'; -import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers/notification'; +import { normalizeNotification } from 'pl-fe/normalizers/notification'; +import { appendFollowRequest } from 'pl-fe/queries/accounts/use-follow-requests'; import { getFilters, regexFromFilters } from 'pl-fe/selectors'; import { useSettingsStore } from 'pl-fe/stores/settings'; import { isLoggedIn } from 'pl-fe/utils/auth'; @@ -18,13 +19,11 @@ import { importEntities } from './importer'; import { saveMarker } from './markers'; import { saveSettings } from './settings'; -import type { Account, Notification as BaseNotification, PaginatedResponse, Status } from 'pl-api'; +import type { Notification as BaseNotification, GetGroupedNotificationsParams, GroupedNotificationsResults, NotificationGroup, PaginatedResponse } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const; const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP' as const; -const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE' as const; -const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE' as const; const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST' as const; const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS' as const; @@ -32,15 +31,8 @@ const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL' as const; const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET' as const; -const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR' as const; const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP' as const; -const NOTIFICATIONS_MARK_READ_REQUEST = 'NOTIFICATIONS_MARK_READ_REQUEST' as const; -const NOTIFICATIONS_MARK_READ_SUCCESS = 'NOTIFICATIONS_MARK_READ_SUCCESS' as const; -const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL' as const; - -const MAX_QUEUED_NOTIFICATIONS = 40; - const FILTER_TYPES = { all: undefined, mention: ['mention'], @@ -58,14 +50,19 @@ 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.account.id); +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; @@ -73,20 +70,32 @@ const updateNotifications = (notification: BaseNotification) => dispatch(importEntities({ accounts: [notification.account, notification.type === 'move' ? notification.target : undefined], - statuses: [getNotificationStatus(notification)], + statuses: [getNotificationStatus(notification) as any], })); + if (showInColumn) { - dispatch({ + const normalizedNotification = normalizeNotification(notification); + + if (normalizedNotification.type === 'follow_request') { + normalizedNotification.sample_account_ids.forEach(appendFollowRequest); + } + + dispatch({ type: NOTIFICATIONS_UPDATE, - notification: normalizeNotification(notification), + notification: normalizedNotification, }); - fetchRelatedRelationships(dispatch, [notification]); + fetchRelatedRelationships(dispatch, [normalizedNotification]); } }; -const updateNotificationsQueue = (notification: BaseNotification, intlMessages: Record, intlLocale: string, curPath: string) => +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 @@ -98,8 +107,6 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages: let filtered: boolean | null = false; - const isOnNotificationsPage = curPath === '/notifications'; - if (notification.type === 'mention' || notification.type === 'status') { const regex = regexFromFilters(filters); const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); @@ -131,43 +138,13 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages: } if (playSound && !filtered) { - dispatch({ + dispatch({ type: NOTIFICATIONS_UPDATE_NOOP, meta: { sound: 'boop' }, }); } - if (isOnNotificationsPage) { - dispatch({ - type: NOTIFICATIONS_UPDATE_QUEUE, - notification, - intlMessages, - intlLocale, - }); - } else { - dispatch(updateNotifications(notification)); - } - }; - -const dequeueNotifications = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const queuedNotifications = getState().notifications.queuedNotifications; - const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount; - - if (totalQueuedNotificationsCount === 0) { - return; - } else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { - queuedNotifications.forEach((block) => { - dispatch(updateNotifications(block.notification)); - }); - } else { - dispatch(expandNotifications()); - } - - dispatch({ - type: NOTIFICATIONS_DEQUEUE, - }); - dispatch(markReadNotifications()); + dispatch(updateNotifications(notification)); }; const excludeTypesFromFilter = (filters: string[]) => NOTIFICATION_TYPES.filter(item => !filters.includes(item)); @@ -195,7 +172,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an } } - const params: Record = { + const params: GetGroupedNotificationsParams = { max_id: maxId, }; @@ -203,7 +180,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an if (features.notificationsIncludeTypes) { params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any)); } else { - params.exclude_types = EXCLUDE_TYPES; + params.exclude_types = [...EXCLUDE_TYPES]; } } else { const filtered = FILTER_TYPES[activeFilter] || [activeFilter]; @@ -214,42 +191,20 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an } } - if (!maxId && notifications.items.size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!maxId && notifications.items.length > 0) { + params.since_id = notifications.items[0]?.page_max_id; } dispatch(expandNotificationsRequest()); - return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => { - const entries = (response.items).reduce((acc, item) => { - if (item.account?.id) { - acc.accounts[item.account.id] = item.account; - } - - // Used by Move notification - if (item.type === 'move' && item.target.id) { - acc.accounts[item.target.id] = item.target; - } - - // TODO actually check for type - // @ts-ignore - if (item.status?.id) { - // @ts-ignore - acc.statuses[item.status.id] = item.status; - } - - return acc; - }, { accounts: {}, statuses: {} } as { accounts: Record; statuses: Record }); - + return getClient(state).groupedNotifications.getGroupedNotifications(params, { signal: abortExpandNotifications.signal }).then(({ items: { accounts, statuses, notification_groups }, next }) => { dispatch(importEntities({ - accounts: Object.values(entries.accounts), - statuses: Object.values(entries.statuses), + accounts, + statuses, })); - const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items); - - dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next)); - fetchRelatedRelationships(dispatch, response.items); + dispatch(expandNotificationsSuccess(notification_groups, next)); + fetchRelatedRelationships(dispatch, notification_groups); done(); }).catch(error => { dispatch(expandNotificationsFail(error)); @@ -259,7 +214,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an const expandNotificationsRequest = () => ({ type: NOTIFICATIONS_EXPAND_REQUEST }); -const expandNotificationsSuccess = (notifications: Array, next: (() => Promise>) | null) => ({ +const expandNotificationsSuccess = (notifications: Array, next: (() => Promise>) | null) => ({ type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, @@ -270,15 +225,24 @@ const expandNotificationsFail = (error: unknown) => ({ error, }); +interface NotificationsScrollTopAction { + type: typeof NOTIFICATIONS_SCROLL_TOP; + top: boolean; +} + const scrollTopNotifications = (top: boolean) => (dispatch: AppDispatch) => { - dispatch({ + dispatch(markReadNotifications()); + return dispatch({ type: NOTIFICATIONS_SCROLL_TOP, top, }); - dispatch(markReadNotifications()); }; +interface SetFilterAction { + type: typeof NOTIFICATIONS_FILTER_SET; +} + const setFilter = (filterType: FilterType, abort?: boolean) => (dispatch: AppDispatch) => { const settingsStore = useSettingsStore.getState(); @@ -286,10 +250,10 @@ const setFilter = (filterType: FilterType, abort?: boolean) => settingsStore.changeSetting(['notifications', 'quickFilter', 'active'], filterType); - dispatch({ type: NOTIFICATIONS_FILTER_SET }); dispatch(expandNotifications(undefined, undefined, abort)); - if (activeFilter !== filterType) dispatch(saveSettings()); + + return dispatch({ type: NOTIFICATIONS_FILTER_SET }); }; const markReadNotifications = () => @@ -297,7 +261,7 @@ const markReadNotifications = () => if (!isLoggedIn(getState)) return; const state = getState(); - const topNotificationId = state.notifications.items.first()?.id; + const topNotificationId = state.notifications.items[0]?.page_max_id; const lastReadId = state.notifications.lastRead; if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { @@ -311,30 +275,28 @@ const markReadNotifications = () => } }; +type NotificationsAction = + | NotificationsUpdateAction + | NotificationsUpdateNoopAction + | ReturnType + | ReturnType + | ReturnType + | NotificationsScrollTopAction + | SetFilterAction; + export { NOTIFICATIONS_UPDATE, - NOTIFICATIONS_UPDATE_NOOP, - NOTIFICATIONS_UPDATE_QUEUE, - NOTIFICATIONS_DEQUEUE, NOTIFICATIONS_EXPAND_REQUEST, NOTIFICATIONS_EXPAND_SUCCESS, NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_MARK_READ_REQUEST, - NOTIFICATIONS_MARK_READ_SUCCESS, - NOTIFICATIONS_MARK_READ_FAIL, - MAX_QUEUED_NOTIFICATIONS, type FilterType, updateNotifications, updateNotificationsQueue, - dequeueNotifications, expandNotifications, - expandNotificationsRequest, - expandNotificationsSuccess, - expandNotificationsFail, scrollTopNotifications, setFilter, markReadNotifications, + type NotificationsAction, }; diff --git a/packages/pl-fe/src/actions/oauth.ts b/packages/pl-fe/src/actions/oauth.ts index 652716b62..70b6caf03 100644 --- a/packages/pl-fe/src/actions/oauth.ts +++ b/packages/pl-fe/src/actions/oauth.ts @@ -13,49 +13,20 @@ import { getBaseURL } from 'pl-fe/utils/state'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST' as const; -const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS' as const; -const OAUTH_TOKEN_CREATE_FAIL = 'OAUTH_TOKEN_CREATE_FAIL' as const; +const obtainOAuthToken = (params: GetTokenParams, baseURL?: string) =>{ + const client = new PlApiClient(baseURL || BuildConfig.BACKEND_URL || ''); -const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST' as const; -const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS' as const; -const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL' as const; - -const obtainOAuthToken = (params: GetTokenParams, baseURL?: string) => - (dispatch: AppDispatch) => { - dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params }); - const client = new PlApiClient(baseURL || BuildConfig.BACKEND_URL || ''); - - return client.oauth.getToken(params).then((token) => { - dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token }); - return token; - }).catch(error => { - dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error, skipAlert: true }); - throw error; - }); - }; + return client.oauth.getToken(params); +}; const revokeOAuthToken = (params: RevokeTokenParams) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params }); const baseURL = getBaseURL(getState()); const client = new PlApiClient(baseURL || ''); - return client.oauth.revokeToken(params).then((data) => { - dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data }); - return data; - }).catch(error => { - dispatch({ type: OAUTH_TOKEN_REVOKE_FAIL, params, error }); - throw error; - }); + return client.oauth.revokeToken(params); }; export { - OAUTH_TOKEN_CREATE_REQUEST, - OAUTH_TOKEN_CREATE_SUCCESS, - OAUTH_TOKEN_CREATE_FAIL, - OAUTH_TOKEN_REVOKE_REQUEST, - OAUTH_TOKEN_REVOKE_SUCCESS, - OAUTH_TOKEN_REVOKE_FAIL, obtainOAuthToken, revokeOAuthToken, }; diff --git a/packages/pl-fe/src/actions/pin-statuses.ts b/packages/pl-fe/src/actions/pin-statuses.ts index f1e61bfc0..4103689e0 100644 --- a/packages/pl-fe/src/actions/pin-statuses.ts +++ b/packages/pl-fe/src/actions/pin-statuses.ts @@ -4,55 +4,32 @@ import { getClient } from '../api'; import { importEntities } from './importer'; -import type { Status } from 'pl-api'; +import type { PaginatedResponse, Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST' as const; const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS' as const; -const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL' as const; const fetchPinnedStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; const me = getState().me; - dispatch(fetchPinnedStatusesRequest()); - return getClient(getState()).accounts.getAccountStatuses(me as string, { pinned: true }).then(response => { dispatch(importEntities({ statuses: response.items })); - dispatch(fetchPinnedStatusesSuccess(response.items, null)); - }).catch(error => { - dispatch(fetchPinnedStatusesFail(error)); + dispatch(fetchPinnedStatusesSuccess(response.items, response.next)); }); }; -const fetchPinnedStatusesRequest = () => ({ - type: PINNED_STATUSES_FETCH_REQUEST, -}); - -const fetchPinnedStatusesSuccess = (statuses: Array, next: string | null) => ({ +const fetchPinnedStatusesSuccess = (statuses: Array, next: (() => Promise>) | null) => ({ type: PINNED_STATUSES_FETCH_SUCCESS, statuses, next, }); -const fetchPinnedStatusesFail = (error: unknown) => ({ - type: PINNED_STATUSES_FETCH_FAIL, - error, -}); - -type PinStatusesAction = - ReturnType - | ReturnType - | ReturnType; +type PinStatusesAction = ReturnType; export { - PINNED_STATUSES_FETCH_REQUEST, PINNED_STATUSES_FETCH_SUCCESS, - PINNED_STATUSES_FETCH_FAIL, fetchPinnedStatuses, - fetchPinnedStatusesRequest, - fetchPinnedStatusesSuccess, - fetchPinnedStatusesFail, type PinStatusesAction, }; diff --git a/packages/pl-fe/src/actions/pl-fe.ts b/packages/pl-fe/src/actions/pl-fe.ts index 2b2f4282d..1072cd805 100644 --- a/packages/pl-fe/src/actions/pl-fe.ts +++ b/packages/pl-fe/src/actions/pl-fe.ts @@ -1,7 +1,8 @@ import { createSelector } from 'reselect'; +import * as v from 'valibot'; import { getHost } from 'pl-fe/actions/instance'; -import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config'; +import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config'; import KVStore from 'pl-fe/storage/kv-store'; import { useSettingsStore } from 'pl-fe/stores/settings'; @@ -17,19 +18,15 @@ const PLFE_CONFIG_REMEMBER_SUCCESS = 'PLFE_CONFIG_REMEMBER_SUCCESS' as const; const getPlFeConfig = createSelector([ (state: RootState) => state.plfe, - (state: RootState) => state.auth.client.features, -], (plfe, features) => { - // Do some additional normalization with the state - return normalizePlFeConfig(plfe); -}); +// Do some additional normalization with the state +], (plfe) => v.parse(plFeConfigSchema, plfe)); const rememberPlFeConfig = (host: string | null) => - (dispatch: AppDispatch) => { - return KVStore.getItemOrError(`plfe_config:${host}`).then(plFeConfig => { + (dispatch: AppDispatch) => + KVStore.getItemOrError(`plfe_config:${host}`).then(plFeConfig => { dispatch({ type: PLFE_CONFIG_REMEMBER_SUCCESS, host, plFeConfig }); return plFeConfig; }).catch(() => {}); - }; const fetchFrontendConfigurations = () => (dispatch: AppDispatch, getState: () => RootState) => @@ -103,11 +100,6 @@ export { PLFE_CONFIG_REQUEST_FAIL, PLFE_CONFIG_REMEMBER_SUCCESS, getPlFeConfig, - rememberPlFeConfig, - fetchFrontendConfigurations, fetchPlFeConfig, loadPlFeConfig, - fetchPlFeJson, - importPlFeConfig, - plFeConfigFail, }; diff --git a/packages/pl-fe/src/actions/polls.ts b/packages/pl-fe/src/actions/polls.ts index e7311edc7..43458d058 100644 --- a/packages/pl-fe/src/actions/polls.ts +++ b/packages/pl-fe/src/actions/polls.ts @@ -2,78 +2,21 @@ import { getClient } from '../api'; import { importEntities } from './importer'; -import type { Poll } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST' as const; -const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS' as const; -const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL' as const; - -const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST' as const; -const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS' as const; -const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL' as const; - const vote = (pollId: string, choices: number[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(voteRequest()); - - return getClient(getState()).polls.vote(pollId, choices).then((data) => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).polls.vote(pollId, choices).then((data) => { dispatch(importEntities({ polls: [data] })); - dispatch(voteSuccess(data)); - }).catch(err => dispatch(voteFail(err))); - }; + }); const fetchPoll = (pollId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchPollRequest()); - - return getClient(getState()).polls.getPoll(pollId).then((data) => { + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).polls.getPoll(pollId).then((data) => { dispatch(importEntities({ polls: [data] })); - dispatch(fetchPollSuccess(data)); - }).catch(err => dispatch(fetchPollFail(err))); - }; - -const voteRequest = () => ({ - type: POLL_VOTE_REQUEST, -}); - -const voteSuccess = (poll: Poll) => ({ - type: POLL_VOTE_SUCCESS, - poll, -}); - -const voteFail = (error: unknown) => ({ - type: POLL_VOTE_FAIL, - error, -}); - -const fetchPollRequest = () => ({ - type: POLL_FETCH_REQUEST, -}); - -const fetchPollSuccess = (poll: Poll) => ({ - type: POLL_FETCH_SUCCESS, - poll, -}); - -const fetchPollFail = (error: unknown) => ({ - type: POLL_FETCH_FAIL, - error, -}); + }); export { - POLL_VOTE_REQUEST, - POLL_VOTE_SUCCESS, - POLL_VOTE_FAIL, - POLL_FETCH_REQUEST, - POLL_FETCH_SUCCESS, - POLL_FETCH_FAIL, vote, fetchPoll, - voteRequest, - voteSuccess, - voteFail, - fetchPollRequest, - fetchPollSuccess, - fetchPollFail, }; diff --git a/packages/pl-fe/src/actions/push-notifications/index.ts b/packages/pl-fe/src/actions/push-notifications/index.ts deleted file mode 100644 index f850f7f1f..000000000 --- a/packages/pl-fe/src/actions/push-notifications/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { register } from './registerer'; -export { - SET_BROWSER_SUPPORT, - SET_SUBSCRIPTION, - CLEAR_SUBSCRIPTION, -} from './setter'; diff --git a/packages/pl-fe/src/actions/push-notifications/registerer.ts b/packages/pl-fe/src/actions/push-notifications/registerer.ts index fa61047f6..3d9983dec 100644 --- a/packages/pl-fe/src/actions/push-notifications/registerer.ts +++ b/packages/pl-fe/src/actions/push-notifications/registerer.ts @@ -1,4 +1,4 @@ -import { createPushSubscription, updatePushSubscription } from 'pl-fe/actions/push-subscriptions'; +import { createPushSubscription } from 'pl-fe/actions/push-subscriptions'; import { pushNotificationsSetting } from 'pl-fe/settings'; import { getVapidKey } from 'pl-fe/utils/auth'; import { decode as decodeBase64 } from 'pl-fe/utils/base64'; @@ -44,7 +44,7 @@ const unsubscribe = ({ registration, subscription }: { const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) => (dispatch: AppDispatch, getState: () => RootState) => { - const alerts = getState().push_notifications.alerts.toJS() as Record; + const alerts = getState().push_notifications.alerts; const params = { subscription, data: { alerts } }; if (me) { @@ -135,21 +135,6 @@ const register = () => .catch(console.warn); }; -const saveSettings = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState().push_notifications; - const alerts = state.alerts.toJS(); - const data = { alerts }; - const me = getState().me; - - return dispatch(updatePushSubscription({ data })).then(() => { - if (me) { - pushNotificationsSetting.set(me, data); - } - }).catch(console.warn); - }; - export { register, - saveSettings, }; diff --git a/packages/pl-fe/src/actions/push-subscriptions.ts b/packages/pl-fe/src/actions/push-subscriptions.ts index 332f9bc42..90f0068df 100644 --- a/packages/pl-fe/src/actions/push-subscriptions.ts +++ b/packages/pl-fe/src/actions/push-subscriptions.ts @@ -1,17 +1,12 @@ import { getClient } from '../api'; -import type { CreatePushNotificationsSubscriptionParams, UpdatePushNotificationsSubscriptionParams } from 'pl-api'; +import type { CreatePushNotificationsSubscriptionParams } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const createPushSubscription = (params: CreatePushNotificationsSubscriptionParams) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).pushNotifications.createSubscription(params); -const updatePushSubscription = (params: UpdatePushNotificationsSubscriptionParams) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).pushNotifications.updateSubscription(params); - export { createPushSubscription, - updatePushSubscription, }; diff --git a/packages/pl-fe/src/actions/reports.ts b/packages/pl-fe/src/actions/reports.ts index fa59e0833..acc9be787 100644 --- a/packages/pl-fe/src/actions/reports.ts +++ b/packages/pl-fe/src/actions/reports.ts @@ -6,10 +6,6 @@ import type { Account } from 'pl-fe/normalizers/account'; import type { Status } from 'pl-fe/normalizers/status'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST' as const; -const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS' as const; -const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL' as const; - enum ReportableEntities { ACCOUNT = 'ACCOUNT', STATUS = 'STATUS' @@ -30,38 +26,15 @@ const initReport = (entityType: ReportableEntities, account: Pick }; const submitReport = (accountId: string, statusIds: string[], ruleIds?: string[], comment?: string, forward?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(submitReportRequest()); - - return getClient(getState()).accounts.reportAccount(accountId, { - status_ids: statusIds, - rule_ids: ruleIds, - comment: comment, - forward: forward, - }); - }; - -const submitReportRequest = () => ({ - type: REPORT_SUBMIT_REQUEST, -}); - -const submitReportSuccess = () => ({ - type: REPORT_SUBMIT_SUCCESS, -}); - -const submitReportFail = (error: unknown) => ({ - type: REPORT_SUBMIT_FAIL, - error, -}); + (dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).accounts.reportAccount(accountId, { + status_ids: statusIds, + rule_ids: ruleIds, + comment: comment, + forward: forward, + }); export { ReportableEntities, - REPORT_SUBMIT_REQUEST, - REPORT_SUBMIT_SUCCESS, - REPORT_SUBMIT_FAIL, initReport, submitReport, - submitReportRequest, - submitReportSuccess, - submitReportFail, }; diff --git a/packages/pl-fe/src/actions/scheduled-statuses.ts b/packages/pl-fe/src/actions/scheduled-statuses.ts index 75da24ddf..e2473419c 100644 --- a/packages/pl-fe/src/actions/scheduled-statuses.ts +++ b/packages/pl-fe/src/actions/scheduled-statuses.ts @@ -13,13 +13,12 @@ const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL' as const const SCHEDULED_STATUS_CANCEL_REQUEST = 'SCHEDULED_STATUS_CANCEL_REQUEST' as const; const SCHEDULED_STATUS_CANCEL_SUCCESS = 'SCHEDULED_STATUS_CANCEL_SUCCESS' as const; -const SCHEDULED_STATUS_CANCEL_FAIL = 'SCHEDULED_STATUS_CANCEL_FAIL' as const; const fetchScheduledStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - if (state.status_lists.get('scheduled_statuses')?.isLoading) { + if (state.status_lists.scheduled_statuses?.isLoading) { return; } @@ -36,13 +35,21 @@ const fetchScheduledStatuses = () => }); }; +interface ScheduledStatusCancelRequestAction { + type: typeof SCHEDULED_STATUS_CANCEL_REQUEST; + statusId: string; +} + +interface ScheduledStatusCancelSuccessAction { + type: typeof SCHEDULED_STATUS_CANCEL_SUCCESS; + statusId: string; +} + const cancelScheduledStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, statusId }); + dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, statusId }); return getClient(getState()).scheduledStatuses.cancelScheduledStatus(statusId).then(() => { - dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, statusId }); - }).catch(error => { - dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, statusId, error }); + dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, statusId }); }); }; @@ -63,9 +70,9 @@ const fetchScheduledStatusesFail = (error: unknown) => ({ const expandScheduledStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().status_lists.get('scheduled_statuses')?.next as any as () => Promise> || null; + const next = getState().status_lists.scheduled_statuses?.next as any as () => Promise> || null; - if (next === null || getState().status_lists.get('scheduled_statuses')?.isLoading) { + if (next === null || getState().status_lists.scheduled_statuses?.isLoading) { return; } @@ -93,6 +100,16 @@ const expandScheduledStatusesFail = (error: unknown) => ({ error, }); +type ScheduledStatusesAction = + | ScheduledStatusCancelRequestAction + | ScheduledStatusCancelSuccessAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + export { SCHEDULED_STATUSES_FETCH_REQUEST, SCHEDULED_STATUSES_FETCH_SUCCESS, @@ -102,14 +119,8 @@ export { SCHEDULED_STATUSES_EXPAND_FAIL, SCHEDULED_STATUS_CANCEL_REQUEST, SCHEDULED_STATUS_CANCEL_SUCCESS, - SCHEDULED_STATUS_CANCEL_FAIL, fetchScheduledStatuses, cancelScheduledStatus, - fetchScheduledStatusesRequest, - fetchScheduledStatusesSuccess, - fetchScheduledStatusesFail, expandScheduledStatuses, - expandScheduledStatusesRequest, - expandScheduledStatusesSuccess, - expandScheduledStatusesFail, + type ScheduledStatusesAction, }; diff --git a/packages/pl-fe/src/actions/search.ts b/packages/pl-fe/src/actions/search.ts deleted file mode 100644 index 4efe4cce6..000000000 --- a/packages/pl-fe/src/actions/search.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { useSettingsStore } from 'pl-fe/stores/settings'; - -import { getClient } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; - -import type { Search } from 'pl-api'; -import type { SearchFilter } from 'pl-fe/reducers/search'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const SEARCH_CLEAR = 'SEARCH_CLEAR' as const; -const SEARCH_SHOW = 'SEARCH_SHOW' as const; -const SEARCH_RESULTS_CLEAR = 'SEARCH_RESULTS_CLEAR' as const; - -const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST' as const; -const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS' as const; -const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL' as const; - -const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET' as const; - -const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST' as const; -const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS' as const; -const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL' as const; - -const SEARCH_ACCOUNT_SET = 'SEARCH_ACCOUNT_SET' as const; - -const clearSearch = () => ({ - type: SEARCH_CLEAR, -}); - -const clearSearchResults = () => ({ - type: SEARCH_RESULTS_CLEAR, -}); - -const submitSearch = (value: string, filter?: SearchFilter) => - (dispatch: AppDispatch, getState: () => RootState) => { - const type = filter || getState().search.filter || 'accounts'; - const accountId = getState().search.accountId; - - // An empty search doesn't return any results - if (value.length === 0) { - return dispatch(clearSearchResults()); - } - - dispatch(fetchSearchRequest(value)); - - const params: Record = { - resolve: true, - limit: 20, - type: type as any, - }; - - if (accountId) params.account_id = accountId; - - return getClient(getState()).search.search(value, params).then(response => { - dispatch(importEntities({ accounts: response.accounts, statuses: response.statuses })); - - dispatch(fetchSearchSuccess(response, value, type)); - dispatch(fetchRelationships(response.accounts.map((item) => item.id))); - }).catch(error => { - dispatch(fetchSearchFail(error)); - }); - }; - -const fetchSearchRequest = (value: string) => ({ - type: SEARCH_FETCH_REQUEST, - value, -}); - -const fetchSearchSuccess = (results: Search, searchTerm: string, searchType: SearchFilter) => ({ - type: SEARCH_FETCH_SUCCESS, - results, - searchTerm, - searchType, -}); - -const fetchSearchFail = (error: unknown) => ({ - type: SEARCH_FETCH_FAIL, - error, -}); - -const setFilter = (value: string, filterType: SearchFilter) => - (dispatch: AppDispatch) => { - dispatch(submitSearch(value, filterType)); - - useSettingsStore.getState().changeSetting(['search', 'filter'], filterType); - - return dispatch({ - type: SEARCH_FILTER_SET, - value: filterType, - }); - }; - -const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: () => RootState) => { - if (type === 'links') return; - const value = getState().search.submittedValue; - const offset = getState().search.results[type].length; - const accountId = getState().search.accountId; - - dispatch(expandSearchRequest(type)); - - const params: Record = { - type, - offset, - }; - if (accountId) params.account_id = accountId; - - return getClient(getState()).search.search(value, params).then(response => { - dispatch(importEntities({ accounts: response.accounts, statuses: response.statuses })); - - dispatch(expandSearchSuccess(response, value, type)); - dispatch(fetchRelationships(response.accounts.map((item) => item.id))); - }).catch(error => { - dispatch(expandSearchFail(error)); - }); -}; - -const expandSearchRequest = (searchType: SearchFilter) => ({ - type: SEARCH_EXPAND_REQUEST, - searchType, -}); - -const expandSearchSuccess = (results: Search, searchTerm: string, searchType: Exclude) => ({ - type: SEARCH_EXPAND_SUCCESS, - results, - searchTerm, - searchType, -}); - -const expandSearchFail = (error: unknown) => ({ - type: SEARCH_EXPAND_FAIL, - error, -}); - -const showSearch = () => ({ - type: SEARCH_SHOW, -}); - -const setSearchAccount = (accountId: string | null) => ({ - type: SEARCH_ACCOUNT_SET, - accountId, -}); - -type SearchAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | { - type: typeof SEARCH_FILTER_SET; - path: (['search', 'filter']); - value: SearchFilter; - } - | ReturnType - | ReturnType - -export { - SEARCH_CLEAR, - SEARCH_SHOW, - SEARCH_RESULTS_CLEAR, - SEARCH_FETCH_REQUEST, - SEARCH_FETCH_SUCCESS, - SEARCH_FETCH_FAIL, - SEARCH_FILTER_SET, - SEARCH_EXPAND_REQUEST, - SEARCH_EXPAND_SUCCESS, - SEARCH_EXPAND_FAIL, - SEARCH_ACCOUNT_SET, - clearSearch, - clearSearchResults, - submitSearch, - fetchSearchRequest, - fetchSearchSuccess, - fetchSearchFail, - setFilter, - expandSearch, - expandSearchRequest, - expandSearchSuccess, - expandSearchFail, - showSearch, - setSearchAccount, - type SearchAction, -}; diff --git a/packages/pl-fe/src/actions/security.ts b/packages/pl-fe/src/actions/security.ts index b296ee5c7..16f17e493 100644 --- a/packages/pl-fe/src/actions/security.ts +++ b/packages/pl-fe/src/actions/security.ts @@ -11,153 +11,66 @@ import { normalizeUsername } from 'pl-fe/utils/input'; import { AUTH_LOGGED_OUT, messages } from './auth'; +import type { OauthToken } from 'pl-api'; +import type { Account } from 'pl-fe/normalizers/account'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST' as const; const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS' as const; -const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL' as const; -const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST' as const; const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS' as const; -const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL' as const; - -const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST' as const; -const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS' as const; -const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL' as const; - -const RESET_PASSWORD_CONFIRM_REQUEST = 'RESET_PASSWORD_CONFIRM_REQUEST' as const; -const RESET_PASSWORD_CONFIRM_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS' as const; -const RESET_PASSWORD_CONFIRM_FAIL = 'RESET_PASSWORD_CONFIRM_FAIL' as const; - -const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST' as const; -const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS' as const; -const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL' as const; - -const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST' as const; -const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS' as const; -const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL' as const; - -const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST' as const; -const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS' as const; -const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL' as const; - -const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST' as const; -const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS' as const; -const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL' as const; const fetchOAuthTokens = () => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: FETCH_TOKENS_REQUEST }); - return getClient(getState).settings.getOauthTokens().then((tokens) => { - dispatch({ type: FETCH_TOKENS_SUCCESS, tokens }); - }).catch((e) => { - dispatch({ type: FETCH_TOKENS_FAIL }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.getOauthTokens().then((tokens) => { + dispatch({ type: FETCH_TOKENS_SUCCESS, tokens }); }); - }; const revokeOAuthTokenById = (tokenId: number) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: REVOKE_TOKEN_REQUEST, tokenId }); - return getClient(getState).settings.deleteOauthToken(tokenId).then(() => { - dispatch({ type: REVOKE_TOKEN_SUCCESS, tokenId }); - }).catch(() => { - dispatch({ type: REVOKE_TOKEN_FAIL, tokenId }); + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.deleteOauthToken(tokenId).then(() => { + dispatch({ type: REVOKE_TOKEN_SUCCESS, tokenId }); }); - }; const changePassword = (oldPassword: string, newPassword: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: CHANGE_PASSWORD_REQUEST }); - - return getClient(getState).settings.changePassword(oldPassword, newPassword).then(response => { - dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); - }).catch(error => { - dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); - throw error; - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.changePassword(oldPassword, newPassword); const resetPassword = (usernameOrEmail: string) => (dispatch: AppDispatch, getState: () => RootState) => { const input = normalizeUsername(usernameOrEmail); - dispatch({ type: RESET_PASSWORD_REQUEST }); - return getClient(getState).settings.resetPassword( input.includes('@') ? input : undefined, input.includes('@') ? undefined : input, - ).then(() => { - dispatch({ type: RESET_PASSWORD_SUCCESS }); - }).catch(error => { - dispatch({ type: RESET_PASSWORD_FAIL, error }); - throw error; - }); + ); }; const changeEmail = (email: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: CHANGE_EMAIL_REQUEST, email }); - - return getClient(getState).settings.changeEmail(email, password).then(response => { - dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); - }).catch(error => { - dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); - throw error; - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.changeEmail(email, password); const deleteAccount = (password: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: CHANGE_PASSWORD_REQUEST }); - const account = getLoggedInAccount(getState()); + const account = getLoggedInAccount(getState())!; - dispatch({ type: DELETE_ACCOUNT_REQUEST }); - return getClient(getState).settings.deleteAccount(password).then(response => { - dispatch({ type: DELETE_ACCOUNT_SUCCESS, response }); - dispatch({ type: AUTH_LOGGED_OUT, account }); + return getClient(getState).settings.deleteAccount(password).then(() => { + dispatch({ type: AUTH_LOGGED_OUT, account }); toast.success(messages.loggedOut); - }).catch(error => { - dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true }); - throw error; }); }; const moveAccount = (targetAccount: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: MOVE_ACCOUNT_REQUEST }); - return getClient(getState).settings.moveAccount(targetAccount, password).then(response => { - dispatch({ type: MOVE_ACCOUNT_SUCCESS, response }); - }).catch(error => { - dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true }); - throw error; - }); - }; + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).settings.moveAccount(targetAccount, password); + +type SecurityAction = + | { type: typeof FETCH_TOKENS_SUCCESS; tokens: Array } + | { type: typeof REVOKE_TOKEN_SUCCESS; tokenId: number } + | { type: typeof AUTH_LOGGED_OUT; account: Account } export { - FETCH_TOKENS_REQUEST, FETCH_TOKENS_SUCCESS, - FETCH_TOKENS_FAIL, - REVOKE_TOKEN_REQUEST, REVOKE_TOKEN_SUCCESS, - REVOKE_TOKEN_FAIL, - RESET_PASSWORD_REQUEST, - RESET_PASSWORD_SUCCESS, - RESET_PASSWORD_FAIL, - RESET_PASSWORD_CONFIRM_REQUEST, - RESET_PASSWORD_CONFIRM_SUCCESS, - RESET_PASSWORD_CONFIRM_FAIL, - CHANGE_PASSWORD_REQUEST, - CHANGE_PASSWORD_SUCCESS, - CHANGE_PASSWORD_FAIL, - CHANGE_EMAIL_REQUEST, - CHANGE_EMAIL_SUCCESS, - CHANGE_EMAIL_FAIL, - DELETE_ACCOUNT_REQUEST, - DELETE_ACCOUNT_SUCCESS, - DELETE_ACCOUNT_FAIL, - MOVE_ACCOUNT_REQUEST, - MOVE_ACCOUNT_SUCCESS, - MOVE_ACCOUNT_FAIL, fetchOAuthTokens, revokeOAuthTokenById, changePassword, @@ -165,4 +78,5 @@ export { changeEmail, deleteAccount, moveAccount, + type SecurityAction, }; diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index 9d2fbf957..04e6c526b 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -13,8 +13,6 @@ import type { AppDispatch, RootState } from 'pl-fe/store'; const FE_NAME = 'pl_fe'; -const getAccount = makeGetAccount(); - /** Options when changing/saving settings. */ type SettingOpts = { /** Whether to display an alert when settings are saved. */ @@ -71,7 +69,7 @@ const updateSettingsStore = (settings: any) => }, })); } else { - const accountUrl = getAccount(state, state.me as string)!.url; + const accountUrl = makeGetAccount()(state, state.me as string)!.url; return updateAuthAccount(accountUrl, settings); } diff --git a/packages/pl-fe/src/actions/status-quotes.test.ts b/packages/pl-fe/src/actions/status-quotes.test.ts deleted file mode 100644 index 13a030534..000000000 --- a/packages/pl-fe/src/actions/status-quotes.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { __stub } from 'pl-fe/api'; -import { mockStore, rootState } from 'pl-fe/jest/test-helpers'; -import { StatusListRecord } from 'pl-fe/reducers/status-lists'; - -import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes'; - -const status = { - account: { - id: 'ABDSjI3Q0R8aDaz1U0', - }, - content: 'quoast', - id: 'AJsajx9hY4Q7IKQXEe', - pleroma: { - quote: { - content: '

10

', - id: 'AJmoVikzI3SkyITyim', - }, - }, -}; - -const statusId = 'AJmoVikzI3SkyITyim'; - -describe('fetchStatusQuotes()', () => { - let store: ReturnType; - - beforeEach(() => { - const state = { ...rootState, me: '1234' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const quotes = await import('pl-fe/__fixtures__/status-quotes.json'); - - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, { - link: `; rel='prev'`, - }); - }); - }); - - it('should fetch quotes from the API', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_FETCH_REQUEST', statusId }, - { type: 'POLLS_IMPORT', polls: [] }, - { type: 'ACCOUNTS_IMPORT', accounts: [status.account] }, - { type: 'STATUSES_IMPORT', statuses: [status] }, - { type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null }, - ]; - await store.dispatch(fetchStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_FETCH_REQUEST', statusId }, - { type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') }, - ]; - await store.dispatch(fetchStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); - -describe('expandStatusQuotes()', () => { - let store: ReturnType; - - describe('without a url', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '1234', - status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a url', () => { - beforeEach(() => { - const state = { - ...rootState, - status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }), - me: '1234', - }; - - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const quotes = await import('pl-fe/__fixtures__/status-quotes.json'); - - __stub((mock) => { - mock.onGet('example').reply(200, quotes, { - link: `; rel='prev'`, - }); - }); - }); - - it('should fetch quotes from the API', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId }, - { type: 'POLLS_IMPORT', polls: [] }, - { type: 'ACCOUNTS_IMPORT', accounts: [status.account] }, - { type: 'STATUSES_IMPORT', statuses: [status] }, - { type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null }, - ]; - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('example').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId }, - { type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') }, - ]; - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); diff --git a/packages/pl-fe/src/actions/status-quotes.ts b/packages/pl-fe/src/actions/status-quotes.ts deleted file mode 100644 index f10956691..000000000 --- a/packages/pl-fe/src/actions/status-quotes.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { getClient } from '../api'; - -import { importEntities } from './importer'; - -import type { Status as BaseStatus, PaginatedResponse } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST' as const; -const STATUS_QUOTES_FETCH_SUCCESS = 'STATUS_QUOTES_FETCH_SUCCESS' as const; -const STATUS_QUOTES_FETCH_FAIL = 'STATUS_QUOTES_FETCH_FAIL' as const; - -const STATUS_QUOTES_EXPAND_REQUEST = 'STATUS_QUOTES_EXPAND_REQUEST' as const; -const STATUS_QUOTES_EXPAND_SUCCESS = 'STATUS_QUOTES_EXPAND_SUCCESS' as const; -const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL' as const; - -const noOp = () => new Promise(f => f(null)); - -interface FetchStatusQuotesRequestAction { - type: typeof STATUS_QUOTES_FETCH_REQUEST; - statusId: string; -} - -interface FetchStatusQuotesSuccessAction { - type: typeof STATUS_QUOTES_FETCH_SUCCESS; - statusId: string; - statuses: Array; - next: (() => Promise>) | null; -} - -interface FetchStatusQuotesFailAction { - type: typeof STATUS_QUOTES_FETCH_FAIL; - statusId: string; - error: unknown; -} - -const fetchStatusQuotes = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { - return dispatch(noOp); - } - - const action: FetchStatusQuotesRequestAction = { type: STATUS_QUOTES_FETCH_REQUEST, statusId }; - dispatch(action); - - return getClient(getState).statuses.getStatusQuotes(statusId).then(response => { - dispatch(importEntities({ statuses: response.items })); - const action: FetchStatusQuotesSuccessAction = { - type: STATUS_QUOTES_FETCH_SUCCESS, - statusId, - statuses: response.items, - next: response.next, - }; - return dispatch(action); - }).catch(error => { - const action: FetchStatusQuotesFailAction = { - type: STATUS_QUOTES_FETCH_FAIL, - statusId, - error, - }; - dispatch(action); - }); - }; - -interface ExpandStatusQuotesRequestAction { - type: typeof STATUS_QUOTES_EXPAND_REQUEST; - statusId: string; -} - -interface ExpandStatusQuotesSuccessAction { - type: typeof STATUS_QUOTES_EXPAND_SUCCESS; - statusId: string; - statuses: Array; - next: (() => Promise>) | null; -} - -interface ExpandStatusQuotesFailAction { - type: typeof STATUS_QUOTES_EXPAND_FAIL; - statusId: string; - error: unknown; -} - -const expandStatusQuotes = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().status_lists.get(`quotes:${statusId}`)?.next || null; - - if (next === null || getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { - return dispatch(noOp); - } - - const action: ExpandStatusQuotesRequestAction = { - type: STATUS_QUOTES_EXPAND_REQUEST, - statusId, - }; - dispatch(action); - - return next().then(response => { - dispatch(importEntities({ statuses: response.items })); - const action: ExpandStatusQuotesSuccessAction = { - type: STATUS_QUOTES_EXPAND_SUCCESS, - statusId, - statuses: response.items, - next: response.next, - }; - dispatch(action); - }).catch(error => { - const action: ExpandStatusQuotesFailAction = { - type: STATUS_QUOTES_EXPAND_FAIL, - statusId, - error, - }; - dispatch(action); - }); - }; - -type StatusQuotesAction = - | FetchStatusQuotesRequestAction - | FetchStatusQuotesSuccessAction - | FetchStatusQuotesFailAction - | ExpandStatusQuotesRequestAction - | ExpandStatusQuotesSuccessAction - | ExpandStatusQuotesFailAction; - -export { - STATUS_QUOTES_FETCH_REQUEST, - STATUS_QUOTES_FETCH_SUCCESS, - STATUS_QUOTES_FETCH_FAIL, - STATUS_QUOTES_EXPAND_REQUEST, - STATUS_QUOTES_EXPAND_SUCCESS, - STATUS_QUOTES_EXPAND_FAIL, - fetchStatusQuotes, - expandStatusQuotes, - type StatusQuotesAction, -}; diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index b9a3e6547..882df2302 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -9,7 +9,7 @@ import { setComposeToStatus } from './compose'; import { importEntities } from './importer'; import { deleteFromTimelines } from './timelines'; -import type { CreateStatusParams, Status as BaseStatus } from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus } from 'pl-api'; import type { Status } from 'pl-fe/normalizers/status'; import type { AppDispatch, RootState } from 'pl-fe/store'; import type { IntlShape } from 'react-intl'; @@ -22,44 +22,21 @@ const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST' as const; const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS' as const; const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL' as const; -const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST' as const; -const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS' as const; -const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL' as const; - const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST' as const; const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS' as const; const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL' as const; -const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST' as const; const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS' as const; -const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL' as const; -const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST' as const; const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS' as const; -const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL' as const; -const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST' as const; const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const; -const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL' as const; - -const STATUS_REVEAL_MEDIA = 'STATUS_REVEAL_MEDIA' as const; -const STATUS_HIDE_MEDIA = 'STATUS_HIDE_MEDIA' as const; - -const STATUS_EXPAND_SPOILER = 'STATUS_EXPAND_SPOILER' as const; -const STATUS_COLLAPSE_SPOILER = 'STATUS_COLLAPSE_SPOILER' as const; - -const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST' as const; -const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS' as const; -const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL' as const; -const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO' as const; const STATUS_UNFILTER = 'STATUS_UNFILTER' as const; -const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE' as const; - const createStatus = (params: CreateStatusParams, idempotencyKey: string, statusId: string | null) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId }); + dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId }); return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params)) .then((status) => { @@ -67,7 +44,7 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status const expectsCard = status.scheduled_at === null && !status.card && shouldHaveCard(status); if (status.scheduled_at === null) dispatch(importEntities({ statuses: [{ ...status, expectsCard }] }, { idempotencyKey, withParents: true })); - dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId }); + dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId }); // Poll the backend for the updated card if (expectsCard) { @@ -88,7 +65,7 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status return status; }).catch(error => { - dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!statusId }); + dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!statusId }); throw error; }); }; @@ -96,34 +73,29 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const status = state.statuses.get(statusId)!; - const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined; + const status = state.statuses[statusId]!; + const poll = status.poll_id ? state.polls[status.poll_id] : undefined; - dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); + dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); return getClient(state).statuses.getStatusSource(statusId).then(response => { - dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); + dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false)); useModalsStore.getState().openModal('COMPOSE'); }).catch(error => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); }); }; const fetchStatus = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: STATUS_FETCH_REQUEST, statusId }); - const params = intl && useSettingsStore.getState().settings.autoTranslate ? { language: intl.locale, } : undefined; return getClient(getState()).statuses.getStatus(statusId, params).then(status => { dispatch(importEntities({ statuses: [status] })); - dispatch({ type: STATUS_FETCH_SUCCESS, status }); return status; - }).catch(error => { - dispatch({ type: STATUS_FETCH_FAIL, statusId, error, skipAlert: true }); }); }; @@ -133,13 +105,13 @@ const deleteStatus = (statusId: string, withRedraft = false) => const state = getState(); - const status = state.statuses.get(statusId)!; - const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined; + const status = state.statuses[statusId]!; + const poll = status.poll_id ? state.polls[status.poll_id] : undefined; - dispatch({ type: STATUS_DELETE_REQUEST, params: status }); + dispatch({ type: STATUS_DELETE_REQUEST, params: status }); return getClient(state).statuses.deleteStatus(statusId).then(response => { - dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); + dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); dispatch(deleteFromTimelines(statusId)); if (withRedraft) { @@ -148,7 +120,7 @@ const deleteStatus = (statusId: string, withRedraft = false) => } }) .catch(error => { - dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); + dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); }); }; @@ -157,8 +129,6 @@ const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) => const fetchContext = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: CONTEXT_FETCH_REQUEST, statusId }); - const params = intl && useSettingsStore.getState().settings.autoTranslate ? { language: intl.locale, } : undefined; @@ -167,14 +137,12 @@ const fetchContext = (statusId: string, intl?: IntlShape) => const { ancestors, descendants } = context; const statuses = ancestors.concat(descendants); dispatch(importEntities({ statuses })); - dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); + dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); return context; }).catch(error => { if (error.response?.status === 404) { dispatch(deleteFromTimelines(statusId)); } - - dispatch({ type: CONTEXT_FETCH_FAIL, statusId, error, skipAlert: true }); }); }; @@ -188,11 +156,8 @@ const muteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch({ type: STATUS_MUTE_REQUEST, statusId }); return getClient(getState()).statuses.muteStatus(statusId).then((status) => { - dispatch({ type: STATUS_MUTE_SUCCESS, statusId }); - }).catch(error => { - dispatch({ type: STATUS_MUTE_FAIL, statusId, error }); + dispatch({ type: STATUS_MUTE_SUCCESS, statusId }); }); }; @@ -200,160 +165,82 @@ const unmuteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch({ type: STATUS_UNMUTE_REQUEST, statusId }); return getClient(getState()).statuses.unmuteStatus(statusId).then(() => { - dispatch({ type: STATUS_UNMUTE_SUCCESS, statusId }); - }).catch(error => { - dispatch({ type: STATUS_UNMUTE_FAIL, statusId, error }); + dispatch({ type: STATUS_UNMUTE_SUCCESS, statusId }); }); }; const toggleMuteStatus = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.muted) { - dispatch(unmuteStatus(status.id)); - } else { - dispatch(muteStatus(status.id)); - } - }; + status.muted ? unmuteStatus(status.id) : muteStatus(status.id); -const hideStatusMedia = (statusIds: string[] | string) => { - if (!Array.isArray(statusIds)) { - statusIds = [statusIds]; - } +// let TRANSLATIONS_QUEUE: Set = new Set(); +// let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null; - return { - type: STATUS_HIDE_MEDIA, - statusIds, - }; -}; +// const translateStatus = (statusId: string, targetLanguage: string, lazy?: boolean) => +// (dispatch: AppDispatch, getState: () => RootState) => { +// const client = getClient(getState); +// const features = client.features; -const revealStatusMedia = (statusIds: string[] | string) => { - if (!Array.isArray(statusIds)) { - statusIds = [statusIds]; - } +// const handleTranslateMany = () => { +// const copy = [...TRANSLATIONS_QUEUE]; +// TRANSLATIONS_QUEUE = new Set(); +// if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); - return { - type: STATUS_REVEAL_MEDIA, - statusIds, - }; -}; +// return client.statuses.translateStatuses(copy, targetLanguage).then((response) => { +// response.forEach((translation) => { +// dispatch({ +// type: STATUS_TRANSLATE_SUCCESS, +// statusId: translation.id, +// translation: translation, +// }); -const toggleStatusMediaHidden = (status: Pick) => { - if (status.hidden) { - return revealStatusMedia(status.id); - } else { - return hideStatusMedia(status.id); - } -}; +// copy +// .filter((statusId) => !response.some(({ id }) => id === statusId)) +// .forEach((statusId) => dispatch({ +// type: STATUS_TRANSLATE_FAIL, +// statusId, +// })); +// }); +// }).catch(error => { +// dispatch({ +// type: STATUS_TRANSLATE_FAIL, +// statusId, +// error, +// }); +// }); +// }; -const collapseStatusSpoiler = (statusIds: string[] | string) => { - if (!Array.isArray(statusIds)) { - statusIds = [statusIds]; - } +// if (features.lazyTranslations && lazy) { +// TRANSLATIONS_QUEUE.add(statusId); - return { - type: STATUS_COLLAPSE_SPOILER, - statusIds, - }; -}; +// if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); +// TRANSLATIONS_TIMEOUT = setTimeout(() => handleTranslateMany(), 3000); +// } else if (features.lazyTranslations && TRANSLATIONS_QUEUE.size) { +// TRANSLATIONS_QUEUE.add(statusId); -const expandStatusSpoiler = (statusIds: string[] | string) => { - if (!Array.isArray(statusIds)) { - statusIds = [statusIds]; - } - - return { - type: STATUS_EXPAND_SPOILER, - statusIds, - }; -}; - -let TRANSLATIONS_QUEUE: Set = new Set(); -let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null; - -const translateStatus = (statusId: string, targetLanguage: string, lazy?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - const client = getClient(getState); - const features = client.features; - - dispatch({ type: STATUS_TRANSLATE_REQUEST, statusId }); - - const handleTranslateMany = () => { - const copy = [...TRANSLATIONS_QUEUE]; - TRANSLATIONS_QUEUE = new Set(); - if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); - - return client.statuses.translateStatuses(copy, targetLanguage).then((response) => { - response.forEach((translation) => { - dispatch({ - type: STATUS_TRANSLATE_SUCCESS, - statusId: translation.id, - translation: translation, - }); - - copy - .filter((statusId) => !response.some(({ id }) => id === statusId)) - .forEach((statusId) => dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - })); - }); - }).catch(error => { - dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - error, - }); - }); - }; - - if (features.lazyTranslations && lazy) { - TRANSLATIONS_QUEUE.add(statusId); - - if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); - TRANSLATIONS_TIMEOUT = setTimeout(() => handleTranslateMany(), 3000); - } else if (features.lazyTranslations && TRANSLATIONS_QUEUE.size) { - TRANSLATIONS_QUEUE.add(statusId); - - handleTranslateMany(); - } else { - return client.statuses.translateStatus(statusId, targetLanguage).then(response => { - dispatch({ - type: STATUS_TRANSLATE_SUCCESS, - statusId, - translation: response, - }); - }).catch(error => { - dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - error, - }); - }); - } - }; - -const undoStatusTranslation = (statusId: string) => ({ - type: STATUS_TRANSLATE_UNDO, - statusId, -}); +// handleTranslateMany(); +// } +// }; const unfilterStatus = (statusId: string) => ({ type: STATUS_UNFILTER, statusId, }); -const changeStatusLanguage = (statusId: string, language: string) => ({ - type: STATUS_LANGUAGE_CHANGE, - statusId, - language, -}); - type StatusesAction = - | ReturnType + | { type: typeof STATUS_CREATE_REQUEST; params: CreateStatusParams; idempotencyKey: string; editing: boolean } + | { type: typeof STATUS_CREATE_SUCCESS; status: BaseStatus | ScheduledStatus; params: CreateStatusParams; idempotencyKey: string; editing: boolean } + | { type: typeof STATUS_CREATE_FAIL; error: unknown; params: CreateStatusParams; idempotencyKey: string; editing: boolean } + | { type: typeof STATUS_FETCH_SOURCE_REQUEST } + | { type: typeof STATUS_FETCH_SOURCE_SUCCESS } + | { type: typeof STATUS_FETCH_SOURCE_FAIL; error: unknown } + | { type: typeof STATUS_DELETE_REQUEST; params: Pick } + | { type: typeof STATUS_DELETE_SUCCESS; statusId: string } + | { type: typeof STATUS_DELETE_FAIL; params: Pick; error: unknown } + | { type: typeof CONTEXT_FETCH_SUCCESS; statusId: string; ancestors: Array; descendants: Array } + | { type: typeof STATUS_MUTE_SUCCESS; statusId: string } + | { type: typeof STATUS_UNMUTE_SUCCESS; statusId: string } | ReturnType - | ReturnType; export { STATUS_CREATE_REQUEST, @@ -362,31 +249,13 @@ export { STATUS_FETCH_SOURCE_REQUEST, STATUS_FETCH_SOURCE_SUCCESS, STATUS_FETCH_SOURCE_FAIL, - STATUS_FETCH_REQUEST, - STATUS_FETCH_SUCCESS, - STATUS_FETCH_FAIL, STATUS_DELETE_REQUEST, STATUS_DELETE_SUCCESS, STATUS_DELETE_FAIL, - CONTEXT_FETCH_REQUEST, CONTEXT_FETCH_SUCCESS, - CONTEXT_FETCH_FAIL, - STATUS_MUTE_REQUEST, STATUS_MUTE_SUCCESS, - STATUS_MUTE_FAIL, - STATUS_UNMUTE_REQUEST, STATUS_UNMUTE_SUCCESS, - STATUS_UNMUTE_FAIL, - STATUS_REVEAL_MEDIA, - STATUS_HIDE_MEDIA, - STATUS_EXPAND_SPOILER, - STATUS_COLLAPSE_SPOILER, - STATUS_TRANSLATE_REQUEST, - STATUS_TRANSLATE_SUCCESS, - STATUS_TRANSLATE_FAIL, - STATUS_TRANSLATE_UNDO, STATUS_UNFILTER, - STATUS_LANGUAGE_CHANGE, createStatus, editStatus, fetchStatus, @@ -397,14 +266,6 @@ export { muteStatus, unmuteStatus, toggleMuteStatus, - hideStatusMedia, - revealStatusMedia, - toggleStatusMediaHidden, - expandStatusSpoiler, - collapseStatusSpoiler, - translateStatus, - undoStatusTranslation, unfilterStatus, - changeStatusLanguage, type StatusesAction, }; diff --git a/packages/pl-fe/src/actions/suggestions.ts b/packages/pl-fe/src/actions/suggestions.ts deleted file mode 100644 index 326997169..000000000 --- a/packages/pl-fe/src/actions/suggestions.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getClient } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; -import { insertSuggestionsIntoTimeline } from './timelines'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST' as const; -const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS' as const; -const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL' as const; - -const fetchSuggestions = (limit = 50) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const client = getClient(state); - const me = state.me; - - if (!me) return null; - - if (client.features.suggestions) { - dispatch({ type: SUGGESTIONS_FETCH_REQUEST }); - - return getClient(getState).myAccount.getSuggestions(limit).then((suggestions) => { - const accounts = suggestions.map(({ account }) => account); - - dispatch(importEntities({ accounts })); - dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, suggestions }); - - dispatch(fetchRelationships(accounts.map(({ id }) => id))); - return suggestions; - }).catch(error => { - dispatch({ type: SUGGESTIONS_FETCH_FAIL, error, skipAlert: true }); - throw error; - }); - } else { - // Do nothing - return null; - } - }; - -const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch) => { - dispatch(fetchSuggestions(20))?.then(() => dispatch(insertSuggestionsIntoTimeline())); -}; - -export { - SUGGESTIONS_FETCH_REQUEST, - SUGGESTIONS_FETCH_SUCCESS, - SUGGESTIONS_FETCH_FAIL, - fetchSuggestions, - fetchSuggestionsForTimeline, -}; diff --git a/packages/pl-fe/src/actions/tags.ts b/packages/pl-fe/src/actions/tags.ts deleted file mode 100644 index bb7c6a3b6..000000000 --- a/packages/pl-fe/src/actions/tags.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { getClient } from '../api'; - -import type { PaginatedResponse, Tag } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST' as const; -const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS' as const; -const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL' as const; - -const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST' as const; -const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS' as const; -const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL' as const; - -const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST' as const; -const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS' as const; -const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL' as const; - -const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST' as const; -const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS' as const; -const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL' as const; - -const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST' as const; -const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS' as const; -const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL' as const; - -const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchHashtagRequest()); - - return getClient(getState()).myAccount.getTag(name).then((data) => { - dispatch(fetchHashtagSuccess(name, data)); - }).catch(err => { - dispatch(fetchHashtagFail(err)); - }); -}; - -const fetchHashtagRequest = () => ({ - type: HASHTAG_FETCH_REQUEST, -}); - -const fetchHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_FETCH_SUCCESS, - name, - tag, -}); - -const fetchHashtagFail = (error: unknown) => ({ - type: HASHTAG_FETCH_FAIL, - error, -}); - -const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(followHashtagRequest(name)); - - return getClient(getState()).myAccount.followTag(name).then((data) => { - dispatch(followHashtagSuccess(name, data)); - }).catch(err => { - dispatch(followHashtagFail(name, err)); - }); -}; - -const followHashtagRequest = (name: string) => ({ - type: HASHTAG_FOLLOW_REQUEST, - name, -}); - -const followHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_FOLLOW_SUCCESS, - name, - tag, -}); - -const followHashtagFail = (name: string, error: unknown) => ({ - type: HASHTAG_FOLLOW_FAIL, - name, - error, -}); - -const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(unfollowHashtagRequest(name)); - - return getClient(getState()).myAccount.unfollowTag(name).then((data) => { - dispatch(unfollowHashtagSuccess(name, data)); - }).catch(err => { - dispatch(unfollowHashtagFail(name, err)); - }); -}; - -const unfollowHashtagRequest = (name: string) => ({ - type: HASHTAG_UNFOLLOW_REQUEST, - name, -}); - -const unfollowHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_UNFOLLOW_SUCCESS, - name, - tag, -}); - -const unfollowHashtagFail = (name: string, error: unknown) => ({ - type: HASHTAG_UNFOLLOW_FAIL, - name, - error, -}); - -const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchFollowedHashtagsRequest()); - - return getClient(getState()).myAccount.getFollowedTags().then(response => { - dispatch(fetchFollowedHashtagsSuccess(response.items, response.next)); - }).catch(err => { - dispatch(fetchFollowedHashtagsFail(err)); - }); -}; - -const fetchFollowedHashtagsRequest = () => ({ - type: FOLLOWED_HASHTAGS_FETCH_REQUEST, -}); - -const fetchFollowedHashtagsSuccess = (followed_tags: Array, next: (() => Promise>) | null) => ({ - type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, - followed_tags, - next, -}); - -const fetchFollowedHashtagsFail = (error: unknown) => ({ - type: FOLLOWED_HASHTAGS_FETCH_FAIL, - error, -}); - -const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().followed_tags.next; - - if (next === null) return; - - dispatch(expandFollowedHashtagsRequest()); - - return next().then(response => { - dispatch(expandFollowedHashtagsSuccess(response.items, response.next)); - }).catch(error => { - dispatch(expandFollowedHashtagsFail(error)); - }); -}; - -const expandFollowedHashtagsRequest = () => ({ - type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, -}); - -const expandFollowedHashtagsSuccess = (followed_tags: Array, next: (() => Promise>) | null) => ({ - type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - followed_tags, - next, -}); - -const expandFollowedHashtagsFail = (error: unknown) => ({ - type: FOLLOWED_HASHTAGS_EXPAND_FAIL, - error, -}); - -export { - HASHTAG_FETCH_REQUEST, - HASHTAG_FETCH_SUCCESS, - HASHTAG_FETCH_FAIL, - HASHTAG_FOLLOW_REQUEST, - HASHTAG_FOLLOW_SUCCESS, - HASHTAG_FOLLOW_FAIL, - HASHTAG_UNFOLLOW_REQUEST, - HASHTAG_UNFOLLOW_SUCCESS, - HASHTAG_UNFOLLOW_FAIL, - FOLLOWED_HASHTAGS_FETCH_REQUEST, - FOLLOWED_HASHTAGS_FETCH_SUCCESS, - FOLLOWED_HASHTAGS_FETCH_FAIL, - FOLLOWED_HASHTAGS_EXPAND_REQUEST, - FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - FOLLOWED_HASHTAGS_EXPAND_FAIL, - fetchHashtag, - fetchHashtagRequest, - fetchHashtagSuccess, - fetchHashtagFail, - followHashtag, - followHashtagRequest, - followHashtagSuccess, - followHashtagFail, - unfollowHashtag, - unfollowHashtagRequest, - unfollowHashtagSuccess, - unfollowHashtagFail, - fetchFollowedHashtags, - fetchFollowedHashtagsRequest, - fetchFollowedHashtagsSuccess, - fetchFollowedHashtagsFail, - expandFollowedHashtags, - expandFollowedHashtagsRequest, - expandFollowedHashtagsSuccess, - expandFollowedHashtagsFail, -}; diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index d3767ea47..a51cd6b64 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -1,5 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; - import { getLocale } from 'pl-fe/actions/settings'; import { useSettingsStore } from 'pl-fe/stores/settings'; import { shouldFilter } from 'pl-fe/utils/timelines'; @@ -8,7 +6,17 @@ import { getClient } from '../api'; import { importEntities } from './importer'; -import type { PaginatedResponse, Status as BaseStatus, PublicTimelineParams, HomeTimelineParams, ListTimelineParams, HashtagTimelineParams, GetAccountStatusesParams, GroupTimelineParams } from 'pl-api'; +import type { + Account as BaseAccount, + GetAccountStatusesParams, + GroupTimelineParams, + HashtagTimelineParams, + HomeTimelineParams, + ListTimelineParams, + PaginatedResponse, + PublicTimelineParams, + Status as BaseStatus, +} from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const; @@ -22,15 +30,13 @@ const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const; const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const; const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; -const TIMELINE_INSERT = 'TIMELINE_INSERT' as const; - const MAX_QUEUED_ITEMS = 40; 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.isEmpty(); + const hasPendingStatuses = !!getState().pending_statuses.length; const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; const shouldSkipQueue = shouldFilter({ @@ -61,28 +67,29 @@ const updateTimeline = (timeline: string, statusId: string) => ({ statusId, }); -const updateTimelineQueue = (timeline: string, statusId: string) => - (dispatch: AppDispatch) => { - // if (typeof accept === 'function' && !accept(status)) { - // return; - // } +const updateTimelineQueue = (timeline: string, statusId: string) => ({ +// if (typeof accept === 'function' && !accept(status)) { +// return; +// } + type: TIMELINE_UPDATE_QUEUE, + timeline, + statusId, +}); - dispatch({ - type: TIMELINE_UPDATE_QUEUE, - timeline, - statusId, - }); - }; +interface TimelineDequeueAction { + type: typeof TIMELINE_DEQUEUE; + timeline: string; +} const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) => void) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const queuedCount = state.timelines.get(timelineId)?.totalQueuedItemsCount || 0; + const queuedCount = state.timelines[timelineId]?.totalQueuedItemsCount || 0; if (queuedCount <= 0) return; if (queuedCount <= MAX_QUEUED_ITEMS) { - dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId }); + dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId }); return; } @@ -105,25 +112,25 @@ interface TimelineDeleteAction { type: typeof TIMELINE_DELETE; statusId: string; accountId: string; - references: ImmutableMap; + references: Array<[string, string]>; reblogOf: string | null; } const deleteFromTimelines = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const accountId = getState().statuses.get(statusId)?.account?.id!; - const references = getState().statuses.filter(status => status.reblog_id === statusId).map(status => [status.id, status.account_id] as const); - const reblogOf = getState().statuses.get(statusId)?.reblog_id || null; + const accountId = getState().statuses[statusId]?.account?.id!; + const references: Array<[string, string]> = Object.entries(getState().statuses) + .filter(([key, status]) => [key, status.reblog_id === statusId]) + .map(([key, status]) => [key, status.account_id]); + const reblogOf = getState().statuses[statusId]?.reblog_id || null; - const action: TimelineDeleteAction = { + dispatch({ type: TIMELINE_DELETE, statusId, accountId, references, reblogOf, - }; - - dispatch(action); + }); }; const clearTimeline = (timeline: string) => ({ type: TIMELINE_CLEAR, timeline }); @@ -134,20 +141,16 @@ const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none (tags[mode] || []).map((tag) => tag.value); const deduplicateStatuses = (statuses: Array) => { - const deduplicatedStatuses: any[] = []; + const deduplicatedStatuses: Array }> = []; for (const status of statuses) { const reblogged = status.reblog && deduplicatedStatuses.find((deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.reblog?.id); if (reblogged) { - if (reblogged.accounts) { - reblogged.accounts.push(status.account); - } else { - reblogged.accounts = [reblogged.account, status.account]; - } + reblogged.accounts.push(status.account); reblogged.id += ':' + status.id; } else if (!deduplicatedStatuses.find((deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.id)) { - deduplicatedStatuses.push(status); + deduplicatedStatuses.push({ accounts: [status.account], ...status }); } } @@ -186,9 +189,9 @@ const fetchHomeTimeline = (expand = false, done = noOp) => const params: HomeTimelineParams = {}; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get('home')?.isLoading) return; + if (expand && state.timelines.home?.isLoading) return; - const fn = (expand && state.timelines.get('home')?.next?.()) || getClient(state).timelines.homeTimeline(params); + const fn = (expand && state.timelines.home?.next?.()) || getClient(state).timelines.homeTimeline(params); return dispatch(handleTimelineExpand('home', fn, false, done)); }; @@ -201,9 +204,9 @@ const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, instance }; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.publicTimeline(params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.publicTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -216,9 +219,9 @@ const fetchBubbleTimeline = ({ onlyMedia }: Record = {}, expand = f const params: PublicTimelineParams = { only_media: onlyMedia }; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.bubbleTimeline(params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.bubbleTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -232,9 +235,9 @@ const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only if (pinned || only_media) params.with_muted = true; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).accounts.getAccountStatuses(accountId, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).accounts.getAccountStatuses(accountId, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -247,9 +250,9 @@ const fetchListTimeline = (listId: string, expand = false, done = noOp) => const params: ListTimelineParams = {}; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.listTimeline(listId, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.listTimeline(listId, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -263,9 +266,9 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = { none: parseTags(tags, 'none'), }; - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -324,12 +327,17 @@ const scrollTopTimeline = (timeline: string, top: boolean) => ({ top, }); -const insertSuggestionsIntoTimeline = () => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: TIMELINE_INSERT, timeline: 'home' }); -}; - // TODO: other actions -type TimelineAction = TimelineDeleteAction; +type TimelineAction = + | ReturnType + | TimelineDeleteAction + | ReturnType + | ReturnType + | TimelineDequeueAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType; export { TIMELINE_UPDATE, @@ -341,11 +349,8 @@ export { TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_FAIL, - TIMELINE_INSERT, MAX_QUEUED_ITEMS, processTimelineUpdate, - updateTimeline, - updateTimelineQueue, dequeueTimeline, deleteFromTimelines, clearTimeline, @@ -356,10 +361,7 @@ export { fetchListTimeline, fetchGroupTimeline, fetchHashtagTimeline, - expandTimelineRequest, expandTimelineSuccess, - expandTimelineFail, scrollTopTimeline, - insertSuggestionsIntoTimeline, type TimelineAction, }; diff --git a/packages/pl-fe/src/actions/trending-statuses.ts b/packages/pl-fe/src/actions/trending-statuses.ts deleted file mode 100644 index 008e5b47f..000000000 --- a/packages/pl-fe/src/actions/trending-statuses.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getClient } from '../api'; - -import { importEntities } from './importer'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST' as const; -const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS' as const; -const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL' as const; - -const fetchTrendingStatuses = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const client = getClient(state); - - if (!client.features.trendingStatuses) return; - - dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST }); - - return client.trends.getTrendingStatuses().then((statuses) => { - dispatch(importEntities({ statuses })); - dispatch({ type: TRENDING_STATUSES_FETCH_SUCCESS, statuses }); - return statuses; - }).catch(error => { - dispatch({ type: TRENDING_STATUSES_FETCH_FAIL, error }); - }); - }; - -export { - TRENDING_STATUSES_FETCH_REQUEST, - TRENDING_STATUSES_FETCH_SUCCESS, - TRENDING_STATUSES_FETCH_FAIL, - fetchTrendingStatuses, -}; diff --git a/packages/pl-fe/src/actions/trends.ts b/packages/pl-fe/src/actions/trends.ts deleted file mode 100644 index 831d94850..000000000 --- a/packages/pl-fe/src/actions/trends.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Tag } from 'pl-api'; - -const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; - -const fetchTrendsSuccess = (tags: Array) => ({ - type: TRENDS_FETCH_SUCCESS, - tags, -}); - -type TrendsAction = ReturnType; - -export { - TRENDS_FETCH_SUCCESS, - fetchTrendsSuccess, - type TrendsAction, -}; diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account-list.ts b/packages/pl-fe/src/api/hooks/accounts/use-account-list.ts index a0ea24fe1..62f808def 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account-list.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account-list.ts @@ -28,7 +28,7 @@ const useAccountList = (listKey: string[], entityFn: EntityFn) => { getNextPageParam: (config) => config.next ? config : undefined, }); - const data = flattenPages(queryInfo.data as any)?.toReversed() || []; + const data = flattenPages(queryInfo.data as any) || []; const { relationships } = useRelationships( listKey, 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 index ac3806700..86abdc638 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts @@ -8,7 +8,8 @@ import { useFeatures } from 'pl-fe/hooks/use-features'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import { type Account, normalizeAccount } from 'pl-fe/normalizers/account'; -import { useAccountScrobble } from './use-account-scrobble'; +import { useAccountScrobble } from '../../../queries/accounts/use-account-scrobble'; + import { useRelationship } from './use-relationship'; import type { Account as BaseAccount } from 'pl-api'; 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 2f27e7ef7..75f0e3ac7 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account.ts @@ -9,7 +9,8 @@ import { useFeatures } from 'pl-fe/hooks/use-features'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import { type Account, normalizeAccount } from 'pl-fe/normalizers/account'; -import { useAccountScrobble } from './use-account-scrobble'; +import { useAccountScrobble } from '../../../queries/accounts/use-account-scrobble'; + import { useRelationship } from './use-relationship'; import type { Account as BaseAccount } from 'pl-api'; diff --git a/packages/pl-fe/src/api/hooks/admin/use-announcements.ts b/packages/pl-fe/src/api/hooks/admin/use-announcements.ts deleted file mode 100644 index c313e0895..000000000 --- a/packages/pl-fe/src/api/hooks/admin/use-announcements.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; -import { - adminAnnouncementSchema, - type AdminAnnouncement, - type AdminCreateAnnouncementParams, - type AdminUpdateAnnouncementParams, -} from 'pl-api'; -import * as v from 'valibot'; - -import { useClient } from 'pl-fe/hooks/use-client'; -import { queryClient } from 'pl-fe/queries/client'; - -import { useAnnouncements as useUserAnnouncements } from '../announcements/use-announcements'; - -const useAnnouncements = () => { - const client = useClient(); - const userAnnouncements = useUserAnnouncements(); - - const getAnnouncements = async () => { - const data = await client.admin.announcements.getAnnouncements(); - - return data.items; - }; - - const result = useQuery>({ - queryKey: ['admin', 'announcements'], - queryFn: getAnnouncements, - placeholderData: [] as ReadonlyArray, - }); - - const { - mutate: createAnnouncement, - isPending: isCreating, - } = useMutation({ - mutationFn: (params: AdminCreateAnnouncementParams) => client.admin.announcements.createAnnouncement(params), - retry: false, - onSuccess: (data) => - queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => - [...prevResult, v.parse(adminAnnouncementSchema, data)], - ), - onSettled: () => userAnnouncements.refetch(), - }); - - const { - mutate: updateAnnouncement, - isPending: isUpdating, - } = useMutation({ - mutationFn: ({ id, ...params }: AdminUpdateAnnouncementParams & { id: string }) => - client.admin.announcements.updateAnnouncement(id, params), - retry: false, - onSuccess: (data) => - queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => - prevResult.map((announcement) => announcement.id === data.id ? v.parse(adminAnnouncementSchema, data) : announcement), - ), - onSettled: () => userAnnouncements.refetch(), - }); - - const { - mutate: deleteAnnouncement, - isPending: isDeleting, - } = useMutation({ - mutationFn: (id: string) => client.admin.announcements.deleteAnnouncement(id), - retry: false, - onSuccess: (_, id) => - queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => - prevResult.filter(({ id: announcementId }) => announcementId !== id), - ), - onSettled: () => userAnnouncements.refetch(), - }); - - return { - ...result, - createAnnouncement, - isCreating, - updateAnnouncement, - isUpdating, - deleteAnnouncement, - isDeleting, - }; -}; - -export { useAnnouncements }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-block-group-member.ts b/packages/pl-fe/src/api/hooks/groups/use-block-group-member.ts deleted file mode 100644 index 673b261dd..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-block-group-member.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useCreateEntity } from 'pl-fe/entity-store/hooks/use-create-entity'; -import { useClient } from 'pl-fe/hooks/use-client'; - -import type { Group } from 'pl-api'; -import type { Account } from 'pl-fe/normalizers/account'; - -const useBlockGroupMember = (group: Pick, account: Pick) => { - const client = useClient(); - - const { createEntity } = useCreateEntity( - [Entities.GROUP_MEMBERSHIPS, account.id], - (accountIds: string[]) => client.experimental.groups.blockGroupUsers(group.id, accountIds), - ); - - return createEntity; -}; - -export { useBlockGroupMember }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-members.test.ts b/packages/pl-fe/src/api/hooks/groups/use-group-members.test.ts deleted file mode 100644 index d6dbf53dc..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group-members.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { GroupRoles } from 'pl-api'; - -import { __stub } from 'pl-fe/api'; -import { buildGroupMember } from 'pl-fe/jest/factory'; -import { renderHook, waitFor } from 'pl-fe/jest/test-helpers'; - -import { useGroupMembers } from './use-group-members'; - -const groupMember = buildGroupMember(); -const groupId = '1'; - -describe('useGroupMembers hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).reply(200, [groupMember]); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groupMembers.length).toBe(1); - expect(result.current.groupMembers[0].id).toBe(groupMember.id); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groupMembers.length).toBe(0); - expect(result.current.isError).toBeTruthy(); - }); - }); -}); diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-members.ts b/packages/pl-fe/src/api/hooks/groups/use-group-members.ts deleted file mode 100644 index d8e8df888..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group-members.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useEntities } from 'pl-fe/entity-store/hooks/use-entities'; -import { useClient } from 'pl-fe/hooks/use-client'; -import { normalizeGroupMember, type GroupMember } from 'pl-fe/normalizers/group-member'; - -import type { GroupMember as BaseGroupMember, GroupRoles } from 'pl-api'; - -const useGroupMembers = (groupId: string, role: GroupRoles) => { - const client = useClient(); - - const { entities, ...result } = useEntities( - [Entities.GROUP_MEMBERSHIPS, groupId, role], - () => client.experimental.groups.getGroupMemberships(groupId, role), - { transform: normalizeGroupMember }, - ); - - return { - ...result, - groupMembers: entities, - }; -}; - -export { useGroupMembers }; diff --git a/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folder.ts b/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folder.ts deleted file mode 100644 index 397517553..000000000 --- a/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folder.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { selectEntity } from 'pl-fe/entity-store/selectors'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; - -import { useBookmarkFolders } from './use-bookmark-folders'; - -import type{ BookmarkFolder } from 'pl-api'; - -const useBookmarkFolder = (folderId?: string) => { - const { - isError, - isFetched, - isFetching, - isLoading, - invalidate, - } = useBookmarkFolders(); - - const bookmarkFolder = useAppSelector(state => folderId - ? selectEntity(state, Entities.BOOKMARK_FOLDERS, folderId) - : undefined); - - return { - bookmarkFolder, - isError, - isFetched, - isFetching, - isLoading, - invalidate, - }; -}; - -export { useBookmarkFolder }; diff --git a/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folders.ts b/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folders.ts deleted file mode 100644 index f936e4f19..000000000 --- a/packages/pl-fe/src/api/hooks/statuses/use-bookmark-folders.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useEntities } from 'pl-fe/entity-store/hooks/use-entities'; -import { useClient } from 'pl-fe/hooks/use-client'; -import { useFeatures } from 'pl-fe/hooks/use-features'; - -import type { BookmarkFolder } from 'pl-api'; - -const useBookmarkFolders = () => { - const client = useClient(); - const features = useFeatures(); - - const { entities, ...result } = useEntities( - [Entities.BOOKMARK_FOLDERS], - () => client.myAccount.getBookmarkFolders(), - { enabled: features.bookmarkFolders }, - ); - - const bookmarkFolders = entities; - - return { - ...result, - bookmarkFolders, - }; -}; - -export { useBookmarkFolders }; diff --git a/packages/pl-fe/src/api/hooks/statuses/use-create-bookmark-folder.ts b/packages/pl-fe/src/api/hooks/statuses/use-create-bookmark-folder.ts deleted file mode 100644 index 8f351906b..000000000 --- a/packages/pl-fe/src/api/hooks/statuses/use-create-bookmark-folder.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useCreateEntity } from 'pl-fe/entity-store/hooks/use-create-entity'; -import { useClient } from 'pl-fe/hooks/use-client'; - -interface CreateBookmarkFolderParams { - name: string; - emoji?: string; -} - -const useCreateBookmarkFolder = () => { - const client = useClient(); - - const { createEntity, ...rest } = useCreateEntity( - [Entities.BOOKMARK_FOLDERS], - (params: CreateBookmarkFolderParams) => - client.myAccount.createBookmarkFolder(params), - ); - - return { - createBookmarkFolder: createEntity, - ...rest, - }; -}; - -export { useCreateBookmarkFolder }; diff --git a/packages/pl-fe/src/api/hooks/statuses/use-delete-bookmark-folder.ts b/packages/pl-fe/src/api/hooks/statuses/use-delete-bookmark-folder.ts deleted file mode 100644 index feaf10793..000000000 --- a/packages/pl-fe/src/api/hooks/statuses/use-delete-bookmark-folder.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useDeleteEntity } from 'pl-fe/entity-store/hooks/use-delete-entity'; -import { useClient } from 'pl-fe/hooks/use-client'; - -const useDeleteBookmarkFolder = () => { - const client = useClient(); - - const { deleteEntity, isSubmitting } = useDeleteEntity( - Entities.BOOKMARK_FOLDERS, - (folderId: string) => client.myAccount.deleteBookmarkFolder(folderId), - ); - - return { - deleteBookmarkFolder: deleteEntity, - isSubmitting, - }; -}; - -export { useDeleteBookmarkFolder }; diff --git a/packages/pl-fe/src/api/hooks/statuses/use-update-bookmark-folder.ts b/packages/pl-fe/src/api/hooks/statuses/use-update-bookmark-folder.ts deleted file mode 100644 index 4fa91b784..000000000 --- a/packages/pl-fe/src/api/hooks/statuses/use-update-bookmark-folder.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useCreateEntity } from 'pl-fe/entity-store/hooks/use-create-entity'; -import { useClient } from 'pl-fe/hooks/use-client'; - -interface UpdateBookmarkFolderParams { - name: string; - emoji?: string; -} - -const useUpdateBookmarkFolder = (folderId: string) => { - const client = useClient(); - - const { createEntity, ...rest } = useCreateEntity( - [Entities.BOOKMARK_FOLDERS], - (params: UpdateBookmarkFolderParams) => - client.myAccount.updateBookmarkFolder(folderId, params), - ); - - return { - updateBookmarkFolder: createEntity, - ...rest, - }; -}; - -export { useUpdateBookmarkFolder }; 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 eb717f8bd..8804f0741 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 @@ -19,7 +19,7 @@ import { useSettingsStore } from 'pl-fe/stores/settings'; import { getUnreadChatsCount, updateChatListItem } from 'pl-fe/utils/chats'; import { play, soundCache } from 'pl-fe/utils/sounds'; -import { updateReactions } from '../announcements/use-announcements'; +import { updateReactions } from '../../../queries/announcements/use-announcements'; import { useTimelineStream } from './use-timeline-stream'; @@ -118,14 +118,7 @@ const useUserStream = () => { break; case 'notification': messages[getLocale()]().then(messages => { - dispatch( - updateNotificationsQueue( - event.payload, - messages, - getLocale(), - window.location.pathname, - ), - ); + dispatch(updateNotificationsQueue(event.payload, messages, getLocale())); }).catch(error => { console.error(error); }); diff --git a/packages/pl-fe/src/api/hooks/trends/use-trending-links.ts b/packages/pl-fe/src/api/hooks/trends/use-trending-links.ts deleted file mode 100644 index 9d7d31fc3..000000000 --- a/packages/pl-fe/src/api/hooks/trends/use-trending-links.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Entities } from 'pl-fe/entity-store/entities'; -import { useEntities } from 'pl-fe/entity-store/hooks/use-entities'; -import { useClient } from 'pl-fe/hooks/use-client'; -import { useFeatures } from 'pl-fe/hooks/use-features'; - -import type { TrendsLink } from 'pl-api'; - -const useTrendingLinks = () => { - const client = useClient(); - const features = useFeatures(); - - const { entities, ...rest } = useEntities( - [Entities.TRENDS_LINKS], - () => client.trends.getTrendingLinks(), - { enabled: features.trendingLinks }, - ); - - return { trendingLinks: entities, ...rest }; -}; - -export { useTrendingLinks }; diff --git a/packages/pl-fe/src/api/index.ts b/packages/pl-fe/src/api/index.ts index 819ba870d..efd01cce9 100644 --- a/packages/pl-fe/src/api/index.ts +++ b/packages/pl-fe/src/api/index.ts @@ -14,7 +14,7 @@ type PlfeResponse = Response & { data: string; json: T }; * No authorization is needed. */ const staticFetch = (input: URL | RequestInfo, init?: RequestInit | undefined) => { - const fullPath = buildFullPath(input.toString(), BuildConfig.FE_SUBDIRECTORY); + const fullPath = buildFullPath(input.toString(), BuildConfig.BACKEND_URL); return fetch(fullPath, init).then(async (response) => { if (!response.ok) throw { response }; diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index d4b52b06c..c0cb0211b 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -85,7 +85,6 @@ interface IAccount { /** Override other actions for specificity like mute/unmute. */ actionType?: 'muting' | 'blocking' | 'follow_request' | 'biting'; avatarSize?: number; - hidden?: boolean; hideActions?: boolean; id?: string; onActionClick?: (account: AccountSchema) => void; @@ -115,7 +114,6 @@ const Account = ({ actionTitle, actionAlignment = 'center', avatarSize = 42, - hidden = false, hideActions = false, onActionClick, showAccountHoverCard = true, @@ -181,15 +179,6 @@ const Account = ({ return null; } - if (hidden) { - return ( - <> - {account.display_name} - {account.username} - - ); - } - if (withDate) timestamp = account.created_at; const LinkEl: any = withLinkToProfile ? Link : 'div'; @@ -203,7 +192,7 @@ const Account = ({
-
+
{emoji && ( {children}} > - + {emoji && ( diff --git a/packages/pl-fe/src/components/announcements/announcement.tsx b/packages/pl-fe/src/components/announcements/announcement.tsx index f0536fec2..4f0d574bc 100644 --- a/packages/pl-fe/src/components/announcements/announcement.tsx +++ b/packages/pl-fe/src/components/announcements/announcement.tsx @@ -9,12 +9,11 @@ import { getTextDirection } from 'pl-fe/utils/rtl'; import AnnouncementContent from './announcement-content'; import ReactionsBar from './reactions-bar'; -import type { Map as ImmutableMap } from 'immutable'; import type { Announcement as AnnouncementEntity, CustomEmoji } from 'pl-api'; interface IAnnouncement { announcement: AnnouncementEntity; - emojiMap: ImmutableMap; + emojiMap: Record; } const Announcement: React.FC = ({ announcement, emojiMap }) => { diff --git a/packages/pl-fe/src/components/announcements/announcements-panel.tsx b/packages/pl-fe/src/components/announcements/announcements-panel.tsx index 558b99a38..cb1d6c511 100644 --- a/packages/pl-fe/src/components/announcements/announcements-panel.tsx +++ b/packages/pl-fe/src/components/announcements/announcements-panel.tsx @@ -1,25 +1,22 @@ import clsx from 'clsx'; -import { Map as ImmutableMap } from 'immutable'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import ReactSwipeableViews from 'react-swipeable-views'; -import { createSelector } from 'reselect'; -import { useAnnouncements } from 'pl-fe/api/hooks/announcements/use-announcements'; import Card from 'pl-fe/components/ui/card'; import HStack from 'pl-fe/components/ui/hstack'; import Widget from 'pl-fe/components/ui/widget'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useAnnouncements } from 'pl-fe/queries/announcements/use-announcements'; +import { useCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis'; import Announcement from './announcement'; import type { CustomEmoji } from 'pl-api'; -import type { RootState } from 'pl-fe/store'; -const customEmojiMap = createSelector([(state: RootState) => state.custom_emojis], items => items.reduce((map, emoji) => map.set(emoji.shortcode, emoji), ImmutableMap())); +const makeCustomEmojiMap = (items: Array) => items.reduce>((map, emoji) => (map[emoji.shortcode] = emoji, map), {}); const AnnouncementsPanel = () => { - const emojiMap = useAppSelector(state => customEmojiMap(state)); + const { data: emojiMap = {} } = useCustomEmojis(makeCustomEmojiMap); const [index, setIndex] = useState(0); const { data: announcements } = useAnnouncements(); diff --git a/packages/pl-fe/src/components/announcements/emoji.tsx b/packages/pl-fe/src/components/announcements/emoji.tsx index cc2de4f89..cf2017072 100644 --- a/packages/pl-fe/src/components/announcements/emoji.tsx +++ b/packages/pl-fe/src/components/announcements/emoji.tsx @@ -4,12 +4,11 @@ import unicodeMapping from 'pl-fe/features/emoji/mapping'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { joinPublicPath } from 'pl-fe/utils/static'; -import type { Map as ImmutableMap } from 'immutable'; import type { CustomEmoji } from 'pl-api'; interface IEmoji { emoji: string; - emojiMap: ImmutableMap; + emojiMap: Record; hovered: boolean; } @@ -31,8 +30,8 @@ const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { src={joinPublicPath(`packs/emoji/${filename}.svg`)} /> ); - } else if (emojiMap.get(emoji)) { - const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); + } else if (emojiMap[emoji]) { + const filename = (autoPlayGif || hovered) ? emojiMap[emoji].url : emojiMap[emoji].static_url; const shortCode = `:${emoji}:`; return ( diff --git a/packages/pl-fe/src/components/announcements/reaction.tsx b/packages/pl-fe/src/components/announcements/reaction.tsx index 983e774c5..ac6d817c2 100644 --- a/packages/pl-fe/src/components/announcements/reaction.tsx +++ b/packages/pl-fe/src/components/announcements/reaction.tsx @@ -1,19 +1,18 @@ import clsx from 'clsx'; import React, { useState } from 'react'; -import { useAnnouncements } from 'pl-fe/api/hooks/announcements/use-announcements'; import AnimatedNumber from 'pl-fe/components/animated-number'; import unicodeMapping from 'pl-fe/features/emoji/mapping'; +import { useAnnouncements } from 'pl-fe/queries/announcements/use-announcements'; import Emoji from './emoji'; -import type { Map as ImmutableMap } from 'immutable'; import type { AnnouncementReaction, CustomEmoji } from 'pl-api'; interface IReaction { announcementId: string; reaction: AnnouncementReaction; - emojiMap: ImmutableMap; + emojiMap: Record; style: React.CSSProperties; } diff --git a/packages/pl-fe/src/components/announcements/reactions-bar.tsx b/packages/pl-fe/src/components/announcements/reactions-bar.tsx index 36b605626..3d56c774d 100644 --- a/packages/pl-fe/src/components/announcements/reactions-bar.tsx +++ b/packages/pl-fe/src/components/announcements/reactions-bar.tsx @@ -1,21 +1,19 @@ -import clsx from 'clsx'; import React from 'react'; import { TransitionMotion, spring } from 'react-motion'; -import { useAnnouncements } from 'pl-fe/api/hooks/announcements/use-announcements'; import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import { useAnnouncements } from 'pl-fe/queries/announcements/use-announcements'; import Reaction from './reaction'; -import type { Map as ImmutableMap } from 'immutable'; import type { AnnouncementReaction, CustomEmoji } from 'pl-api'; import type { Emoji, NativeEmoji } from 'pl-fe/features/emoji'; interface IReactionsBar { announcementId: string; reactions: Array; - emojiMap: ImmutableMap; + emojiMap: Record; } const ReactionsBar: React.FC = ({ announcementId, reactions, emojiMap }) => { @@ -41,7 +39,7 @@ const ReactionsBar: React.FC = ({ announcementId, reactions, emoj return ( {items => ( -
+
{items.map(({ key, data, style }) => ( { const fallback =
; const onOpenMedia = (media: Array, index: number) => openModal('MEDIA', { media, index }); - const visible = isMediaVisible(status, displayMedia); + const visible = useMediaVisible(status, displayMedia); return (
diff --git a/packages/pl-fe/src/components/autosuggest-account-input.tsx b/packages/pl-fe/src/components/autosuggest-account-input.tsx index ed166d14e..fadff8f28 100644 --- a/packages/pl-fe/src/components/autosuggest-account-input.tsx +++ b/packages/pl-fe/src/components/autosuggest-account-input.tsx @@ -1,4 +1,3 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; import throttle from 'lodash/throttle'; import React, { useState, useRef, useCallback, useEffect } from 'react'; @@ -12,6 +11,7 @@ import type { InputThemes } from 'pl-fe/components/ui/input'; const noOp = () => { }; interface IAutosuggestAccountInput { + id?: string; onChange: React.ChangeEventHandler; onSelected: (accountId: string) => void; autoFocus?: boolean; @@ -21,6 +21,7 @@ interface IAutosuggestAccountInput { menu?: Menu; onKeyDown?: React.KeyboardEventHandler; theme?: InputThemes; + placeholder?: string; } const AutosuggestAccountInput: React.FC = ({ @@ -30,7 +31,7 @@ const AutosuggestAccountInput: React.FC = ({ ...rest }) => { const dispatch = useAppDispatch(); - const [accountIds, setAccountIds] = useState(ImmutableOrderedSet()); + const [accountIds, setAccountIds] = useState>([]); const controller = useRef(new AbortController()); const refreshCancelToken = () => { @@ -39,14 +40,14 @@ const AutosuggestAccountInput: React.FC = ({ }; const clearResults = () => { - setAccountIds(ImmutableOrderedSet()); + setAccountIds([]); }; const handleAccountSearch = useCallback(throttle((q) => { dispatch(accountSearch(q, controller.current.signal)) .then((accounts: { id: string }[]) => { const accountIds = accounts.map(account => account.id); - setAccountIds(ImmutableOrderedSet(accountIds)); + setAccountIds(accountIds); }) .catch(noOp); }, 900, { leading: true, trailing: true }), []); @@ -79,7 +80,7 @@ const AutosuggestAccountInput: React.FC = ({ , 'onChange' | 'onKeyUp' | 'onKeyDown'> { +interface IAutosuggestInput extends Pick, 'lang' | 'onChange' | 'onKeyUp' | 'onKeyDown'> { value: string; - suggestions: ImmutableList; + suggestions: Array; disabled?: boolean; placeholder?: string; onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void; onSuggestionsClearRequested: () => void; onSuggestionsFetchRequested: (token: string) => void; - autoFocus: boolean; - autoSelect: boolean; + autoFocus?: boolean; + autoSelect?: boolean; className?: string; id?: string; - searchTokens: string[]; + searchTokens?: string[]; maxLength?: number; menu?: Menu; - renderSuggestion?: React.FC<{ id: string }>; hidePortal?: boolean; theme?: InputThemes; } -class AutosuggestInput extends PureComponent { +const AutosuggestInput: React.FC = ({ + autoFocus = false, + autoSelect = true, + searchTokens = ['@', ':', '#'], + ...props +}) => { + const getFirstIndex = () => autoSelect ? 0 : -1; - static defaultProps = { - autoFocus: false, - autoSelect: true, - searchTokens: ImmutableList(['@', ':', '#']), - }; + const [suggestionsHidden, setSuggestionsHidden] = useState(true); + const [focused, setFocused] = useState(false); + const [selectedSuggestion, setSelectedSuggestion] = useState(getFirstIndex()); + const [lastToken, setLastToken] = useState(null); + const [tokenStart, setTokenStart] = useState(0); - getFirstIndex = () => this.props.autoSelect ? 0 : -1; + const inputRef = useRef(null); - state = { - suggestionsHidden: true, - focused: false, - selectedSuggestion: this.getFirstIndex(), - lastToken: null, - tokenStart: 0, - }; - - input: HTMLInputElement | null = null; - - onChange: React.ChangeEventHandler = (e) => { + const onChange: React.ChangeEventHandler = (e) => { const [tokenStart, token] = textAtCursorMatchesToken( e.target.value, e.target.selectionStart || 0, - this.props.searchTokens, + searchTokens, ); - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); + if (token !== null && lastToken !== token) { + setLastToken(token); + setSelectedSuggestion(0); + setTokenStart(tokenStart); + props.onSuggestionsFetchRequested(token); } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); + setLastToken(null); + props.onSuggestionsClearRequested(); } - if (this.props.onChange) { - this.props.onChange(e); + if (props.onChange) { + props.onChange(e); } }; - onKeyDown: React.KeyboardEventHandler = (e) => { - const { suggestions, menu, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - const firstIndex = this.getFirstIndex(); - const lastIndex = suggestions.size + (menu || []).length - 1; + const onKeyDown: React.KeyboardEventHandler = (e) => { + const { suggestions, menu, disabled } = props; + const firstIndex = getFirstIndex(); + const lastIndex = suggestions.length + (menu || []).length - 1; if (disabled) { e.preventDefault(); @@ -94,97 +92,86 @@ class AutosuggestInput extends PureComponent { switch (e.key) { case 'Escape': - if (suggestions.size === 0 || suggestionsHidden) { + if (suggestions.length === 0 || suggestionsHidden) { document.querySelector('.ui')?.parentElement?.focus(); } else { e.preventDefault(); - this.setState({ suggestionsHidden: true }); + setSuggestionsHidden(true); } break; case 'ArrowDown': - if (!suggestionsHidden && (suggestions.size > 0 || menu)) { + if (!suggestionsHidden && (suggestions.length > 0 || menu)) { e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) }); + setSelectedSuggestion((selectedSuggestion) => Math.min(selectedSuggestion + 1, lastIndex)); } break; case 'ArrowUp': - if (!suggestionsHidden && (suggestions.size > 0 || menu)) { + if (!suggestionsHidden && (suggestions.length > 0 || menu)) { e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) }); + setSelectedSuggestion((selectedSuggestion) => Math.min(selectedSuggestion - 1, lastIndex)); } break; case 'Enter': case 'Tab': // Select suggestion - if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) { + if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.length > 0 || menu)) { e.preventDefault(); e.stopPropagation(); - this.setState({ selectedSuggestion: firstIndex }); + setSelectedSuggestion(firstIndex); - if (selectedSuggestion < suggestions.size) { - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + if (selectedSuggestion < suggestions.length) { + props.onSuggestionSelected(tokenStart!, lastToken, suggestions[selectedSuggestion]); } else if (menu) { - const item = menu[selectedSuggestion - suggestions.size]; - this.handleMenuItemAction(item, e); + const item = menu[selectedSuggestion - suggestions.length]; + handleMenuItemAction(item, e); } } break; } - if (e.defaultPrevented || !this.props.onKeyDown) { + if (e.defaultPrevented || !props.onKeyDown) { return; } - if (this.props.onKeyDown) { - this.props.onKeyDown(e); + if (props.onKeyDown) { + props.onKeyDown(e); } }; - onBlur = () => { - this.setState({ suggestionsHidden: true, focused: false }); + const onBlur = () => { + setSuggestionsHidden(true); + setFocused(true); }; - onFocus = () => { - this.setState({ focused: true }); + const onFocus = () => { + setFocused(true); }; - onSuggestionClick: React.EventHandler = (e) => { + const onSuggestionClick: React.EventHandler = (e) => { const index = Number(e.currentTarget?.getAttribute('data-index')); - const suggestion = this.props.suggestions.get(index); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.input?.focus(); + const suggestion = props.suggestions[index]; + props.onSuggestionSelected(tokenStart!, lastToken, suggestion); + inputRef.current?.focus(); e.preventDefault(); }; - componentDidUpdate(prevProps: IAutosuggestInput, prevState: any) { - const { suggestions } = this.props; - if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) { - this.setState({ suggestionsHidden: false }); - } - } + useEffect(() => { + if (suggestionsHidden && focused) setSuggestionsHidden(false); + }, [props.suggestions]); - setInput = (c: HTMLInputElement) => { - this.input = c; - }; - - renderSuggestion = (suggestion: AutoSuggestion, i: number) => { - const { selectedSuggestion } = this.state; + const renderSuggestion = (suggestion: AutoSuggestion, i: number) => { let inner, key; - if (this.props.renderSuggestion && typeof suggestion === 'string') { - const RenderSuggestion = this.props.renderSuggestion; - inner = ; - key = suggestion; + if (typeof suggestion === 'object' && 'origin_id' in suggestion) { + inner = ; + key = suggestion.origin_id; } else if (typeof suggestion === 'object') { inner = ; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; } else { inner = ; key = suggestion; @@ -201,29 +188,28 @@ class AutosuggestInput extends PureComponent { 'hover:bg-gray-100 dark:hover:bg-gray-800': i !== selectedSuggestion, 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion, })} - onMouseDown={this.onSuggestionClick} - onTouchEnd={this.onSuggestionClick} + onMouseDown={onSuggestionClick} + onTouchEnd={onSuggestionClick} > {inner}
); }; - handleMenuItemAction = (item: MenuItem | null, e: React.MouseEvent | React.KeyboardEvent) => { - this.onBlur(); + const handleMenuItemAction = (item: MenuItem | null, e: React.MouseEvent | React.KeyboardEvent) => { + onBlur(); if (item?.action) { item.action(e); } }; - handleMenuItemClick = (item: MenuItem | null): React.MouseEventHandler => e => { + const handleMenuItemClick = (item: MenuItem | null): React.MouseEventHandler => e => { e.preventDefault(); - this.handleMenuItemAction(item, e); + handleMenuItemAction(item, e); }; - renderMenu = () => { - const { menu, suggestions } = this.props; - const { selectedSuggestion } = this.state; + const renderMenu = () => { + const { menu, suggestions } = props; if (!menu) { return null; @@ -231,11 +217,13 @@ class AutosuggestInput extends PureComponent { return menu.map((item, i) => ( {item?.icon && ( @@ -247,66 +235,61 @@ class AutosuggestInput extends PureComponent { )); }; - setPortalPosition() { - if (!this.input) { + const setPortalPosition = () => { + if (!inputRef.current) { return {}; } - const { top, height, left, width } = this.input.getBoundingClientRect(); + const { top, height, left, width } = inputRef.current.getBoundingClientRect(); return { left, width, top: top + height }; - } + }; - render() { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props; - const { suggestionsHidden } = this.state; + const visible = !suggestionsHidden && (props.suggestions.length || (props.menu && props.value)); - const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value)); + return [ +
+ - return [ -
- - - -
, - -
-
- {suggestions.map(this.renderSuggestion)} -
- - {this.renderMenu()} + +
, + +
+
+ {props.suggestions.map(renderSuggestion)}
- , - ]; - } -} + {renderMenu()} +
+
, + ]; +}; export { type AutoSuggestion, type IAutosuggestInput, AutosuggestInput as default }; diff --git a/packages/pl-fe/src/components/autosuggest-location.tsx b/packages/pl-fe/src/components/autosuggest-location.tsx index 77f9f30ec..22e692fbf 100644 --- a/packages/pl-fe/src/components/autosuggest-location.tsx +++ b/packages/pl-fe/src/components/autosuggest-location.tsx @@ -4,7 +4,8 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; + +import type { Location } from 'pl-api'; const buildingCommunityIcon = require('@tabler/icons/outline/building-community.svg'); const homeIcon = require('@tabler/icons/outline/home-2.svg'); @@ -21,12 +22,10 @@ const ADDRESS_ICONS: Record = { }; interface IAutosuggestLocation { - id: string; + location: Location; } -const AutosuggestLocation: React.FC = ({ id }) => { - const location = useAppSelector((state) => state.locations.get(id)); - +const AutosuggestLocation: React.FC = ({ location }) => { if (!location) return null; return ( diff --git a/packages/pl-fe/src/components/avatar-stack.tsx b/packages/pl-fe/src/components/avatar-stack.tsx index 6cfd85065..8d1bd01e4 100644 --- a/packages/pl-fe/src/components/avatar-stack.tsx +++ b/packages/pl-fe/src/components/avatar-stack.tsx @@ -1,23 +1,18 @@ import clsx from 'clsx'; -import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; -import { makeGetAccount } from 'pl-fe/selectors'; - -import type { Account } from 'pl-fe/normalizers/account'; - -const getAccount = makeGetAccount(); +import { selectAccounts } from 'pl-fe/selectors'; interface IAvatarStack { - accountIds: ImmutableOrderedSet; + accountIds: Array; limit?: number; } const AvatarStack: React.FC = ({ accountIds, limit = 3 }) => { - const accounts = useAppSelector(state => ImmutableList(accountIds.slice(0, limit).map(accountId => getAccount(state, accountId)).filter(account => account))) as ImmutableList; + const accounts = useAppSelector(state => selectAccounts(state, accountIds.slice(0, limit))); return ( diff --git a/packages/pl-fe/src/components/birthday-input.tsx b/packages/pl-fe/src/components/birthday-input.tsx index 7928abf9d..58688074e 100644 --- a/packages/pl-fe/src/components/birthday-input.tsx +++ b/packages/pl-fe/src/components/birthday-input.tsx @@ -29,7 +29,7 @@ const BirthdayInput: React.FC = ({ value, onChange, required }) const minAge = instance.pleroma.metadata.birthday_min_age; const maxDate = useMemo(() => { - if (!supportsBirthdays) return null; + if (!supportsBirthdays) return undefined; let maxDate = new Date(); maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60); @@ -108,7 +108,7 @@ const BirthdayInput: React.FC = ({ value, onChange, required })
); - const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : ''); + const handleChange = (date: Date | null) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : ''); return (
diff --git a/packages/pl-fe/src/components/birthday-panel.tsx b/packages/pl-fe/src/components/birthday-panel.tsx index 1e1d579a4..33f7d6f7a 100644 --- a/packages/pl-fe/src/components/birthday-panel.tsx +++ b/packages/pl-fe/src/components/birthday-panel.tsx @@ -1,12 +1,9 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchBirthdayReminders } from 'pl-fe/actions/accounts'; import Widget from 'pl-fe/components/ui/widget'; import AccountContainer from 'pl-fe/containers/account-container'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useBirthdayReminders } from 'pl-fe/queries/accounts/use-birthday-reminders'; const timeToMidnight = () => { const now = new Date(); @@ -15,31 +12,36 @@ const timeToMidnight = () => { return midnight.getTime() - now.getTime(); }; +const getCurrentDate = () => { + const date = new Date(); + + const day = date.getDate(); + const month = date.getMonth() + 1; + + return [day, month]; +}; + interface IBirthdayPanel { limit: number; } const BirthdayPanel = ({ limit }: IBirthdayPanel) => { - const dispatch = useAppDispatch(); + const [[day, month], setDate] = useState(getCurrentDate); - const birthdays: ImmutableOrderedSet = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items || ImmutableOrderedSet()); + const { data: birthdays = [] } = useBirthdayReminders(month, day); const birthdaysToRender = birthdays.slice(0, limit); const timeout = useRef(); - const handleFetchBirthdayReminders = () => { - const date = new Date(); - - const day = date.getDate(); - const month = date.getMonth() + 1; - - dispatch(fetchBirthdayReminders(month, day))?.then(() => { - timeout.current = setTimeout(() => handleFetchBirthdayReminders(), timeToMidnight()); - }); - }; - React.useEffect(() => { - handleFetchBirthdayReminders(); + const updateTimeout = () => { + timeout.current = setTimeout(() => { + setDate(getCurrentDate); + updateTimeout(); + }, timeToMidnight()); + }; + + updateTimeout(); return () => { if (timeout.current) { @@ -48,7 +50,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => { }; }, []); - if (birthdaysToRender.isEmpty()) { + if (!birthdaysToRender.length) { return null; } @@ -66,4 +68,4 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => { ); }; -export { BirthdayPanel as default }; +export { BirthdayPanel as default, getCurrentDate }; diff --git a/packages/pl-fe/src/components/event-preview.tsx b/packages/pl-fe/src/components/event-preview.tsx index 81692d410..3d457cbad 100644 --- a/packages/pl-fe/src/components/event-preview.tsx +++ b/packages/pl-fe/src/components/event-preview.tsx @@ -42,7 +42,7 @@ const EventPreview: React.FC = ({ status, className, hideAction, diff --git a/packages/pl-fe/src/components/fork-awesome-icon.tsx b/packages/pl-fe/src/components/fork-awesome-icon.tsx index f86048d73..c1681184b 100644 --- a/packages/pl-fe/src/components/fork-awesome-icon.tsx +++ b/packages/pl-fe/src/components/fork-awesome-icon.tsx @@ -14,21 +14,13 @@ interface IForkAwesomeIcon extends React.HTMLAttributes { fixedWidth?: boolean; } -const ForkAwesomeIcon: React.FC = ({ id, className, fixedWidth, ...rest }) => { - // Use the Fork Awesome retweet icon, but change its alt - // tag. There is a common adblocker rule which hides elements with - // alt='retweet' unless the domain is twitter.com. This should - // change what screenreaders call it as well. - // const alt = (id === 'retweet') ? 'repost' : id; - - return ( - - ); -}; +const ForkAwesomeIcon: React.FC = ({ id, className, fixedWidth, ...rest }) => ( + +); export { ForkAwesomeIcon as default }; diff --git a/packages/pl-fe/src/components/groups/group-avatar.tsx b/packages/pl-fe/src/components/groups/group-avatar.tsx index 4ffeacf86..110d5c8a8 100644 --- a/packages/pl-fe/src/components/groups/group-avatar.tsx +++ b/packages/pl-fe/src/components/groups/group-avatar.tsx @@ -18,7 +18,7 @@ const GroupAvatar = (props: IGroupAvatar) => { return ( { } isFlush - children={ -
{children}
- } - /> + > +
{children}
+ ); }; diff --git a/packages/pl-fe/src/components/hashtags-bar.tsx b/packages/pl-fe/src/components/hashtags-bar.tsx new file mode 100644 index 000000000..7da4a400b --- /dev/null +++ b/packages/pl-fe/src/components/hashtags-bar.tsx @@ -0,0 +1,62 @@ +// Adapted from Mastodon https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/components/hashtag_bar.tsx +import React, { useCallback, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import HStack from './ui/hstack'; +import Text from './ui/text'; + +// Fit on a single line on desktop +const VISIBLE_HASHTAGS = 3; + +interface IHashtagsBar { + hashtags: Array; +} + +const HashtagsBar: React.FC = ({ hashtags }) => { + const [expanded, setExpanded] = useState(false); + const handleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + + setExpanded(true); + }, []); + + if (hashtags.length === 0) { + return null; + } + + const revealedHashtags = expanded + ? hashtags + : hashtags.slice(0, VISIBLE_HASHTAGS); + + return ( + + {revealedHashtags.map((hashtag) => ( + e.stopPropagation()} + className='flex items-center rounded-sm bg-gray-100 px-1.5 py-1 text-center text-xs font-medium text-primary-600 black:bg-primary-900 dark:bg-primary-700 dark:text-white' + > + + #{hashtag} + + + ))} + + {!expanded && hashtags.length > VISIBLE_HASHTAGS && ( + + )} + + ); +}; + +export { HashtagsBar as default }; diff --git a/packages/pl-fe/src/components/helmet.tsx b/packages/pl-fe/src/components/helmet.tsx index cc3df2a76..e1d242fdd 100644 --- a/packages/pl-fe/src/components/helmet.tsx +++ b/packages/pl-fe/src/components/helmet.tsx @@ -11,8 +11,8 @@ FaviconService.initFaviconService(); const getNotifTotals = (state: RootState): number => { const notifications = state.notifications.unread || 0; - const reports = state.admin.openReports.count(); - const approvals = state.admin.awaitingApproval.count(); + const reports = state.admin.openReports.length; + const approvals = state.admin.awaitingApproval.length; return notifications + reports + approvals; }; diff --git a/packages/pl-fe/src/components/location-search.tsx b/packages/pl-fe/src/components/location-search.tsx index 444c7c25c..4766a10a8 100644 --- a/packages/pl-fe/src/components/location-search.tsx +++ b/packages/pl-fe/src/components/location-search.tsx @@ -1,15 +1,13 @@ +import { useDebounce } from '@uidotdev/usehooks'; import clsx from 'clsx'; -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import throttle from 'lodash/throttle'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { locationSearch } from 'pl-fe/actions/events'; import AutosuggestInput, { AutoSuggestion } from 'pl-fe/components/autosuggest-input'; import Icon from 'pl-fe/components/icon'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useSearchLocation } from 'pl-fe/queries/search/use-search-location'; -import AutosuggestLocation from './autosuggest-location'; +import type { Location } from 'pl-api'; const noOp = () => {}; @@ -18,27 +16,24 @@ const messages = defineMessages({ }); interface ILocationSearch { - onSelected: (locationId: string) => void; + onSelected: (location: Location) => void; } const LocationSearch: React.FC = ({ onSelected }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const [locationIds, setLocationIds] = useState(ImmutableOrderedSet()); - const controller = useRef(new AbortController()); const [value, setValue] = useState(''); + const debouncedValue = useDebounce(value, 400); + const locationsQuery = useSearchLocation(debouncedValue); const empty = !(value.length > 0); const handleChange: React.ChangeEventHandler = ({ target }) => { - refreshCancelToken(); - handleLocationSearch(target.value); setValue(target.value); }; const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => { - if (typeof suggestion === 'string') { + if (typeof suggestion === 'object' && 'origin_id' in suggestion) { onSelected(suggestion); } }; @@ -57,30 +52,6 @@ const LocationSearch: React.FC = ({ onSelected }) => { } }; - const refreshCancelToken = () => { - controller.current.abort(); - controller.current = new AbortController(); - }; - - const clearResults = () => { - setLocationIds(ImmutableOrderedSet()); - }; - - const handleLocationSearch = useCallback(throttle(q => { - dispatch(locationSearch(q, controller.current.signal)) - .then((locations: { origin_id: string }[]) => { - const locationIds = locations.map(location => location.origin_id); - setLocationIds(ImmutableOrderedSet(locationIds)); - }) - .catch(noOp); - }, 900, { leading: true, trailing: true }), []); - - useEffect(() => { - if (value === '') { - clearResults(); - } - }, [value]); - return (
diff --git a/packages/pl-fe/src/components/translate-button.tsx b/packages/pl-fe/src/components/translate-button.tsx index 5575d7175..9437b78fe 100644 --- a/packages/pl-fe/src/components/translate-button.tsx +++ b/packages/pl-fe/src/components/translate-button.tsx @@ -1,26 +1,25 @@ import React, { useEffect } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { translateStatus, undoStatusTranslation } from 'pl-fe/actions/statuses'; -import { useTranslationLanguages } from 'pl-fe/api/hooks/instance/use-translation-languages'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import { useTranslationLanguages } from 'pl-fe/queries/instance/use-translation-languages'; +import { useStatusTranslation } from 'pl-fe/queries/statuses/use-status-translation'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import type { Status } from 'pl-fe/normalizers/status'; interface ITranslateButton { - status: Pick; + status: Pick; } const TranslateButton: React.FC = ({ status }) => { - const dispatch = useAppDispatch(); const intl = useIntl(); const features = useFeatures(); const instance = useInstance(); @@ -30,6 +29,10 @@ const TranslateButton: React.FC = ({ status }) => { const me = useAppSelector((state) => state.me); const { translationLanguages } = useTranslationLanguages(); + const { statuses: statusesMeta, fetchTranslation, hideTranslation } = useStatusMetaStore(); + + const targetLanguage = statusesMeta[status.id]?.targetLanguage; + const translationQuery = useStatusTranslation(status.id, targetLanguage); const { allow_remote: allowRemote, @@ -43,45 +46,45 @@ const TranslateButton: React.FC = ({ status }) => { const handleTranslate: React.MouseEventHandler = (e) => { e.stopPropagation(); - if (status.translation) { - dispatch(undoStatusTranslation(status.id)); + if (targetLanguage) { + hideTranslation(status.id); } else { - dispatch(translateStatus(status.id, intl.locale)); + fetchTranslation(status.id, intl.locale); } }; useEffect(() => { - if (status.translation === null && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && status.translation !== false && status.language !== null && !knownLanguages.includes(status.language)) { - dispatch(translateStatus(status.id, intl.locale, true)); + if (translationQuery.data === undefined && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && translationQuery.data !== false && status.language !== null && !knownLanguages.includes(status.language)) { + fetchTranslation(status.id, intl.locale); } }, []); - if (!features.translations || !renderTranslate || !supportsLanguages || status.translation === false) return null; + if (!features.translations || !renderTranslate || !supportsLanguages || translationQuery.data === false) return null; const button = ( ); - if (status.translation) { + if (translationQuery.data) { const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' }); const languageName = languageNames.of(status.language!); - const provider = status.translation.provider; + const provider = translationQuery.data.provider; return ( diff --git a/packages/pl-fe/src/components/ui/avatar.tsx b/packages/pl-fe/src/components/ui/avatar.tsx index 96bb46867..7374c1f9e 100644 --- a/packages/pl-fe/src/components/ui/avatar.tsx +++ b/packages/pl-fe/src/components/ui/avatar.tsx @@ -39,7 +39,7 @@ const Avatar = (props: IAvatar) => { width: size, height: size, }} - className={clsx('flex items-center justify-center rounded-full bg-gray-200 dark:bg-gray-900', className)} + className={clsx('flex items-center justify-center rounded-lg bg-gray-200 dark:bg-gray-900', className)} > { return ( , 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'style' | 'id'> { +interface IInput extends Pick, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'style' | 'id' | 'lang'> { /** Put the cursor into the input on mount. */ autoFocus?: boolean; /** The initial text in the input. */ @@ -48,7 +48,7 @@ interface IInput extends Pick, 'maxL const Input = React.forwardRef( (props, ref) => { const intl = useIntl(); - const locale = useLocale(); + const direction = useLocaleDirection(useLocale()); const { type = 'text', icon, className, outerClassName, append, prepend, theme = 'normal', ...filteredProps } = props; @@ -98,7 +98,7 @@ const Input = React.forwardRef( 'pl-8': typeof icon !== 'undefined', 'pl-16': typeof prepend !== 'undefined', }, className)} - dir={typeof props.value === 'string' ? getTextDirection(props.value, { fallback: locale.direction }) : undefined} + dir={typeof props.value === 'string' ? getTextDirection(props.value, { fallback: direction }) : undefined} /> {append ? ( diff --git a/packages/pl-fe/src/components/ui/textarea.tsx b/packages/pl-fe/src/components/ui/textarea.tsx index 5576ddf4f..8c3e758e1 100644 --- a/packages/pl-fe/src/components/ui/textarea.tsx +++ b/packages/pl-fe/src/components/ui/textarea.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useLocale } from 'pl-fe/hooks/use-locale'; +import { useLocale, useLocaleDirection } from 'pl-fe/hooks/use-locale'; import { getTextDirection } from 'pl-fe/utils/rtl'; import Stack from './stack'; @@ -56,7 +56,7 @@ const Textarea = React.forwardRef(({ }: ITextarea, ref: React.ForwardedRef) => { const length = value?.length || 0; const [rows, setRows] = useState(autoGrow ? minRows : initialRows); - const locale = useLocale(); + const direction = useLocaleDirection(useLocale()); const handleChange = (event: React.ChangeEvent) => { if (autoGrow) { @@ -99,7 +99,7 @@ const Textarea = React.forwardRef(({ 'text-red-600 border-red-600': hasError, 'resize-none': !isResizeable, })} - dir={value?.length ? getTextDirection(value, { fallback: locale.direction }) : undefined} + dir={value?.length ? getTextDirection(value, { fallback: direction }) : undefined} /> {maxLength && ( diff --git a/packages/pl-fe/src/components/upload.tsx b/packages/pl-fe/src/components/upload.tsx index 095407f05..65b238e03 100644 --- a/packages/pl-fe/src/components/upload.tsx +++ b/packages/pl-fe/src/components/upload.tsx @@ -237,7 +237,7 @@ const Upload: React.FC = ({ /> )} -
+
{mediaType === 'video' && (
= ({ onSelected }) => { placeholder={intl.formatMessage(messages.placeholder)} value={value} onChange={handleChange} - suggestions={locationIds.toList()} + suggestions={locationsQuery.data || []} onSuggestionsFetchRequested={noOp} onSuggestionsClearRequested={noOp} onSuggestionSelected={handleSelected} searchTokens={[]} onKeyDown={handleKeyDown} - renderSuggestion={AutosuggestLocation} />
diff --git a/packages/pl-fe/src/components/media-gallery.tsx b/packages/pl-fe/src/components/media-gallery.tsx index 78124f4a4..f9c9136dd 100644 --- a/packages/pl-fe/src/components/media-gallery.tsx +++ b/packages/pl-fe/src/components/media-gallery.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useRef, useLayoutEffect, CSSProperties } from 'react'; import Blurhash from 'pl-fe/components/blurhash'; import Icon from 'pl-fe/components/icon'; @@ -12,25 +12,24 @@ import { truncateFilename } from 'pl-fe/utils/media'; import { isIOS } from '../is-mobile'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio'; -import type { Property } from 'csstype'; import type { MediaAttachment } from 'pl-api'; const ATTACHMENT_LIMIT = 4; const MAX_FILENAME_LENGTH = 45; interface Dimensions { - w: Property.Width | number; - h: Property.Height | number; - t?: Property.Top; - r?: Property.Right; - b?: Property.Bottom; - l?: Property.Left; - float?: Property.Float; - pos?: Property.Position; + w: CSSProperties['width']; + h: CSSProperties['height']; + t?: CSSProperties['top']; + r?: CSSProperties['right']; + b?: CSSProperties['bottom']; + l?: CSSProperties['left']; + float?: CSSProperties['float']; + pos?: CSSProperties['position']; } interface SizeData { - style: React.CSSProperties; + style: CSSProperties; itemsDimensions: Dimensions[]; size: number; width: number; @@ -322,7 +321,7 @@ const MediaGallery: React.FC = (props) => { const panoSize = Math.floor(w / maximumAspectRatio); const panoSize_px = `${Math.floor(w / maximumAspectRatio)}px`; - const style: React.CSSProperties = {}; + const style: CSSProperties = {}; let itemsDimensions: Dimensions[] = []; const ratios = Array(size).fill(null).map((_, i) => getAspectRatio(media[i])); diff --git a/packages/pl-fe/src/components/modal-root.tsx b/packages/pl-fe/src/components/modal-root.tsx index d0e1f57d4..be0eb10fc 100644 --- a/packages/pl-fe/src/components/modal-root.tsx +++ b/packages/pl-fe/src/components/modal-root.tsx @@ -10,7 +10,7 @@ import { usePrevious } from 'pl-fe/hooks/use-previous'; import { useModalsStore } from 'pl-fe/stores/modals'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; -import type { ReducerCompose } from 'pl-fe/reducers/compose'; +import type { Compose } from 'pl-fe/reducers/compose'; const messages = defineMessages({ confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' }, @@ -18,22 +18,14 @@ const messages = defineMessages({ saveDraft: { id: 'confirmations.cancel_editing.save_draft', defaultMessage: 'Save draft' }, }); -const checkComposeContent = (compose?: ReturnType) => +const checkComposeContent = (compose?: Compose) => !!compose && [ compose.editorState && compose.editorState.length > 0, compose.spoiler_text.length > 0, - compose.media_attachments.size > 0, + compose.media_attachments.length > 0, compose.poll !== null, ].some(check => check === true); -// const checkEventComposeContent = (compose?: ReturnType) => -// !!compose && [ -// compose.name.length > 0, -// compose.status.length > 0, -// compose.location !== null, -// compose.banner !== null, -// ].some(check => check === true); - interface IModalRoot { onCancel?: () => void; onClose: (type?: ModalType) => void; @@ -67,9 +59,8 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const handleOnClose = () => { dispatch((_, getState) => { - const compose = getState().compose.get('compose-modal'); + const compose = getState().compose['compose-modal']; const hasComposeContent = checkComposeContent(compose); - // const hasEventComposeContent = checkEventComposeContent(getState().compose_event); if (hasComposeContent && type === 'COMPOSE') { const isEditing = compose!.id !== null; @@ -95,26 +86,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) dispatch(cancelReplyCompose()); }, }); - // TODO: restore this functionality - // } else if (hasEventComposeContent && type === 'COMPOSE_EVENT') { - // const isEditing = getState().compose_event.id !== null; - // openModal('CONFIRM', { - // heading: isEditing - // ? - // : , - // message: isEditing - // ? - // : , - // confirm: intl.formatMessage(isEditing ? messages.cancelEditing : messages.confirm), - // onConfirm: () => { - // onClose('COMPOSE_EVENT'); - // dispatch(cancelEventCompose()); - // }, - // onCancel: () => { - // onClose('CONFIRM'); - // }, - // }); - } else if ((hasComposeContent/* || hasEventComposeContent */) && type === 'CONFIRM') { + } else if (hasComposeContent && type === 'CONFIRM') { onClose('CONFIRM'); } else { onClose(); diff --git a/packages/pl-fe/src/components/navlinks.tsx b/packages/pl-fe/src/components/navlinks.tsx index ee84fd150..8a49c5ef0 100644 --- a/packages/pl-fe/src/components/navlinks.tsx +++ b/packages/pl-fe/src/components/navlinks.tsx @@ -16,7 +16,7 @@ const Navlinks: React.FC = ({ type }) => { return (
- {navlinks.get(type)?.map((link, idx) => { + {navlinks[type]?.map((link, idx) => { const url = link.url; const isExternal = url.startsWith('http'); const Comp = (isExternal ? 'a' : Link) as 'a'; @@ -26,7 +26,7 @@ const Navlinks: React.FC = ({ type }) => {
- {(link.getIn(['titleLocales', locale]) || link.get('title')) as string} + {link.titleLocales[locale] || link.title}
diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index 2ed7bed59..668c48e49 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -1,6 +1,9 @@ +/* eslint-disable no-redeclare */ import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser'; import DOMPurify from 'isomorphic-dompurify'; -import React, { useMemo } from 'react'; +import groupBy from 'lodash/groupBy'; +import minBy from 'lodash/minBy'; +import React from 'react'; import { Link } from 'react-router-dom'; import Emojify from 'pl-fe/features/emoji/emojify'; @@ -26,98 +29,164 @@ interface IParsedContent { emojis?: Array; } -const ParsedContent: React.FC = (({ html, mentions, hasQuote, emojis }) => { - return useMemo(() => { - if (html.length === 0) { - return null; - } +// Adapted from Mastodon https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/components/hashtag_bar.tsx +const normalizeHashtag = (hashtag: string) =>( + !!hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag +).normalize('NFKC'); - const emojiMap = emojis ? makeEmojiMap(emojis) : undefined; +const uniqueHashtagsWithCaseHandling = (hashtags: string[]) => { + const groups = groupBy(hashtags, (tag) => + tag.normalize('NFKD').toLowerCase(), + ); - const selectors: Array = []; + return Object.values(groups).map((tags) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the array has at least one element + const firstTag = tags[0]!; - // Explicit mentions - if (mentions) selectors.push('recipients-inline'); + if (tags.length === 1) return firstTag; - // Quote posting - if (hasQuote) selectors.push('quote-inline'); + // The best match is the one where we have the less difference between upper and lower case letter count + const best = minBy(tags, (tag) => { + const upperCase = Array.from(tag).reduce( + (acc, char) => (acc += char.toUpperCase() === char ? 1 : 0), + 0, + ); - const options: HTMLReactParserOptions = { - replace(domNode) { - if (!(domNode instanceof Element)) { - return; - } + const lowerCase = tag.length - upperCase; - if (['script', 'iframe'].includes(domNode.name)) { - return <>; - } + return Math.abs(lowerCase - upperCase); + }); - if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) { - return <>; - } + return best ?? firstTag; + }); +}; - if (domNode.name === 'a') { - const classes = domNode.attribs.class?.split(' '); +function parseContent(props: IParsedContent): ReturnType; +function parseContent(props: IParsedContent, extractHashtags: true): { + hashtags: Array; + content: ReturnType; +}; - const fallback = ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
e.stopPropagation()} - rel='nofollow noopener' - target='_blank' - title={domNode.attribs.href} - > - {domToReact(domNode.children as DOMNode[], options)} - - ); +function parseContent({ html, mentions, hasQuote, emojis }: IParsedContent, extractHashtags = false) { + if (html.length === 0) { + return extractHashtags ? { content: null, hashtags: [] } : null; + } - if (classes?.includes('mention')) { - if (mentions) { - const mention = mentions.find(({ url }) => domNode.attribs.href === url); - if (mention) { - return ( - - e.stopPropagation()} - > - @{mention.username} - - - ); - } - } else if (domNode.attribs['data-user']) { + const emojiMap = emojis ? makeEmojiMap(emojis) : undefined; + + const selectors: Array = []; + + // Explicit mentions + if (mentions) selectors.push('recipients-inline'); + + // Quote posting + if (hasQuote) selectors.push('quote-inline'); + + const hashtags: Array = []; + + const options: HTMLReactParserOptions = { + replace(domNode, index) { + if (!(domNode instanceof Element)) { + return; + } + + if (['script', 'iframe'].includes(domNode.name)) { + return <>; + } + + if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) { + return <>; + } + + if (domNode.name === 'a') { + const classes = domNode.attribs.class?.split(' '); + + const fallback = ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + e.stopPropagation()} + rel='nofollow noopener' + target='_blank' + title={domNode.attribs.href} + > + {domToReact(domNode.children as DOMNode[], options)} + + ); + + if (classes?.includes('mention')) { + if (mentions) { + const mention = mentions.find(({ url }) => domNode.attribs.href === url); + if (mention) { return ( - + + e.stopPropagation()} + > + @{mention.username} + + ); } + } else if (domNode.attribs['data-user']) { + return ( + + ); } - - if (classes?.includes('hashtag')) { - const hashtag = nodesToText(domNode.children as Array); - if (hashtag) { - return ; - } - } - - return fallback; - } - }, - - transform(reactNode, _domNode, index) { - if (typeof reactNode === 'string') { - return ; } - return reactNode as JSX.Element; - }, - }; + if (classes?.includes('hashtag')) { + const hashtag = nodesToText(domNode.children as Array); + if (hashtag) { + return ; + } + } - return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options); - }, [html]); -}); + return fallback; + } -export { ParsedContent }; + if (extractHashtags && domNode.type === 'tag' && domNode.parent === null && domNode.next === null) { + for (const child of domNode.children) { + switch (child.type) { + case 'text': + if (child.data.trim().length) return; + break; + case 'tag': + if (child.name !== 'a') return; + if (!child.attribs.class?.split(' ').includes('hashtag')) return; + hashtags.push(normalizeHashtag(nodesToText([child]))); + break; + default: + return; + } + } + + return <>; + } + }, + + transform(reactNode, _domNode, index) { + if (typeof reactNode === 'string') { + return ; + } + + return reactNode as JSX.Element; + }, + }; + + const content = parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options); + + if (extractHashtags) return { + content, + hashtags: uniqueHashtagsWithCaseHandling(hashtags), + }; + + return content; +} + +const ParsedContent: React.FC = React.memo((props) => parseContent(props), (prevProps, nextProps) => prevProps.html === nextProps.html); + +export { ParsedContent, parseContent }; diff --git a/packages/pl-fe/src/components/polls/poll.tsx b/packages/pl-fe/src/components/polls/poll.tsx index efe4fdff0..6337c84ab 100644 --- a/packages/pl-fe/src/components/polls/poll.tsx +++ b/packages/pl-fe/src/components/polls/poll.tsx @@ -17,20 +17,21 @@ type Selected = Record; interface IPoll { id: string; - status?: Pick; + status?: Pick; + language?: string; } const messages = defineMessages({ multiple: { id: 'poll.choose_multiple', defaultMessage: 'Choose as many as you\'d like.' }, }); -const Poll: React.FC = ({ id, status }): JSX.Element | null => { +const Poll: React.FC = ({ id, status, language }): JSX.Element | null => { const { openModal } = useModalsStore(); const dispatch = useAppDispatch(); const intl = useIntl(); const isLoggedIn = useAppSelector((state) => state.me); - const poll = useAppSelector((state) => state.polls.get(id)); + const poll = useAppSelector((state) => state.polls[id]); const [selected, setSelected] = useState({} as Selected); @@ -87,7 +88,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { showResults={showResults} active={!!selected[i]} onToggle={toggleOption} - language={status?.currentLanguage} + language={language} /> ))} diff --git a/packages/pl-fe/src/components/quoted-status.tsx b/packages/pl-fe/src/components/quoted-status.tsx index b1f8e388b..fdab7971f 100644 --- a/packages/pl-fe/src/components/quoted-status.tsx +++ b/packages/pl-fe/src/components/quoted-status.tsx @@ -100,7 +100,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => {status.quote_id && } diff --git a/packages/pl-fe/src/components/scroll-top-button.tsx b/packages/pl-fe/src/components/scroll-top-button.tsx index b1d9794cd..b40be263f 100644 --- a/packages/pl-fe/src/components/scroll-top-button.tsx +++ b/packages/pl-fe/src/components/scroll-top-button.tsx @@ -24,7 +24,7 @@ const ScrollTopButton: React.FC = ({ onClick, count, message, - threshold = 400, + threshold = 240, autoloadThreshold = 50, }) => { const intl = useIntl(); diff --git a/packages/pl-fe/src/components/search-input.tsx b/packages/pl-fe/src/components/search-input.tsx new file mode 100644 index 000000000..36e571bf6 --- /dev/null +++ b/packages/pl-fe/src/components/search-input.tsx @@ -0,0 +1,115 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import AutosuggestAccountInput from 'pl-fe/components/autosuggest-account-input'; +import SvgIcon from 'pl-fe/components/ui/svg-icon'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { selectAccount } from 'pl-fe/selectors'; + +import type { AppDispatch, RootState } from 'pl-fe/store'; +import type { History } from 'pl-fe/types/history'; + +const messages = defineMessages({ + placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, + action: { id: 'search.action', defaultMessage: 'Search for “{query}”' }, +}); + +const redirectToAccount = (accountId: string, routerHistory: History) => + (_dispatch: AppDispatch, getState: () => RootState) => { + const acct = selectAccount(getState(), accountId)!.acct; + + if (acct && routerHistory) { + routerHistory.push(`/@${acct}`); + } + }; + +const SearchInput = () => { + const [value, setValue] = useState(''); + + const dispatch = useAppDispatch(); + const history = useHistory(); + const intl = useIntl(); + + const handleChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + setValue(value); + }; + + const handleClear = (event: React.MouseEvent) => { + setValue(''); + }; + + const handleSubmit = () => { + setValue(''); + history.push('/search?' + new URLSearchParams({ q: value })); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + + handleSubmit(); + } else if (event.key === 'Escape') { + document.querySelector('.ui')?.parentElement?.focus(); + } + }; + + const handleSelected = (accountId: string) => { + setValue(''); + dispatch(redirectToAccount(accountId, history)); + }; + + const makeMenu = () => [ + { + text: intl.formatMessage(messages.action, { query: value }), + icon: require('@tabler/icons/outline/search.svg'), + action: handleSubmit, + }, + ]; + + const hasValue = value.length > 0; + + return ( +
+ + +
+ + +
+ + + +
+
+
+ ); +}; + +export { SearchInput as default }; diff --git a/packages/pl-fe/src/components/sidebar-menu.tsx b/packages/pl-fe/src/components/sidebar-menu.tsx index 90e51a2b5..e94ec5ef7 100644 --- a/packages/pl-fe/src/components/sidebar-menu.tsx +++ b/packages/pl-fe/src/components/sidebar-menu.tsx @@ -18,12 +18,13 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useRegistrationStatus } from 'pl-fe/hooks/use-registration-status'; +import { useFollowRequestsCount } from 'pl-fe/queries/accounts/use-follow-requests'; +import { useInteractionRequestsCount } from 'pl-fe/queries/statuses/use-interaction-requests'; import { makeGetOtherAccounts } from 'pl-fe/selectors'; import { useSettingsStore } from 'pl-fe/stores/settings'; import { useUiStore } from 'pl-fe/stores/ui'; import sourceCode from 'pl-fe/utils/code'; -import type { List as ImmutableList } from 'immutable'; import type { Account as AccountEntity } from 'pl-fe/normalizers/account'; const messages = defineMessages({ @@ -42,6 +43,7 @@ const messages = defineMessages({ drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' }, addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, + interactionRequests: { id: 'navigation.interaction_requests', defaultMessage: 'Interaction requests' }, close: { id: 'lightbox.close', defaultMessage: 'Close' }, login: { id: 'account.login', defaultMessage: 'Log in' }, register: { id: 'account.register', defaultMessage: 'Sign up' }, @@ -93,11 +95,12 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const features = useFeatures(); const me = useAppSelector((state) => state.me); const { account } = useAccount(me || undefined); - const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); + const otherAccounts = useAppSelector((state) => getOtherAccounts(state)); const { settings } = useSettingsStore(); - const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); - const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); - const draftCount = useAppSelector((state) => state.draft_statuses.size); + const followRequestsCount = useFollowRequestsCount().data || 0; + const interactionRequestsCount = useInteractionRequestsCount().data || 0; + const scheduledStatusCount = useAppSelector((state) => Object.keys(state.scheduled_statuses).length); + const draftCount = useAppSelector((state) => Object.keys(state.draft_statuses).length); // const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); const [sidebarVisible, setSidebarVisible] = useState(isSidebarOpen); const touchStart = useRef(0); @@ -232,6 +235,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { /> )} + {(interactionRequestsCount > 0) && ( + + )} + {features.conversations && ( { const logoSrc = useLogo(); const notificationCount = useAppSelector((state) => state.notifications.unread); - const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); - const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); - const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); - const draftCount = useAppSelector((state) => state.draft_statuses.size); + const followRequestsCount = useFollowRequestsCount().data || 0; + const interactionRequestsCount = useInteractionRequestsCount().data || 0; + const dashboardCount = useAppSelector((state) => state.admin.openReports.length + state.admin.awaitingApproval.length); + const scheduledStatusCount = useAppSelector((state) => Object.keys(state.scheduled_statuses).length); + const draftCount = useAppSelector((state) => Object.keys(state.draft_statuses).length); const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated; @@ -71,6 +75,15 @@ const SidebarNavigation = () => { }); } + if (interactionRequestsCount > 0) { + menu.push({ + to: '/interaction_requests', + text: intl.formatMessage(messages.interactionRequests), + icon: require('@tabler/icons/outline/flag-question.svg'), + count: interactionRequestsCount, + }); + } + if (features.bookmarks) { menu.push({ to: '/bookmarks', diff --git a/packages/pl-fe/src/components/site-error-boundary.tsx b/packages/pl-fe/src/components/site-error-boundary.tsx index 2b2ce2241..cd8ab2428 100644 --- a/packages/pl-fe/src/components/site-error-boundary.tsx +++ b/packages/pl-fe/src/components/site-error-boundary.tsx @@ -158,20 +158,20 @@ const SiteErrorBoundary: React.FC = ({ children }) => {
- {links.get('status') && ( - + {links.status && ( + )} - {links.get('help') && ( - + {links.help && ( + )} - {links.get('support') && ( - + {links.support && ( + )} diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 2ed5b6fe1..4b30e54fb 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -6,19 +6,15 @@ import { useHistory, useRouteMatch } from 'react-router-dom'; import { blockAccount } from 'pl-fe/actions/accounts'; import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose'; import { emojiReact } from 'pl-fe/actions/emoji-reacts'; -import { editEvent } from 'pl-fe/actions/events'; import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation'; -import { initMuteModal } from 'pl-fe/actions/mutes'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { changeSetting } from 'pl-fe/actions/settings'; -import { deleteStatus, editStatus, toggleMuteStatus, translateStatus, undoStatusTranslation } from 'pl-fe/actions/statuses'; +import { deleteStatus, editStatus, toggleMuteStatus } from 'pl-fe/actions/statuses'; import { deleteFromTimelines } from 'pl-fe/actions/timelines'; -import { useBlockGroupMember } from 'pl-fe/api/hooks/groups/use-block-group-member'; import { useDeleteGroupStatus } from 'pl-fe/api/hooks/groups/use-delete-group-status'; import { useGroup } from 'pl-fe/api/hooks/groups/use-group'; import { useGroupRelationship } from 'pl-fe/api/hooks/groups/use-group-relationship'; -import { useTranslationLanguages } from 'pl-fe/api/hooks/instance/use-translation-languages'; import DropdownMenu from 'pl-fe/components/dropdown-menu'; import StatusActionButton from 'pl-fe/components/status-action-button'; import HStack from 'pl-fe/components/ui/hstack'; @@ -26,16 +22,23 @@ import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dr import { languages } from 'pl-fe/features/preferences'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useCanInteract } from 'pl-fe/hooks/use-can-interact'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { useChats } from 'pl-fe/queries/chats'; +import { useBlockGroupUserMutation } from 'pl-fe/queries/groups/use-group-blocks'; +import { useTranslationLanguages } from 'pl-fe/queries/instance/use-translation-languages'; import { useModalsStore } from 'pl-fe/stores/modals'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import toast from 'pl-fe/toast'; import copy from 'pl-fe/utils/copy'; import GroupPopover from './groups/popover/group-popover'; +import Popover from './ui/popover'; +import Stack from './ui/stack'; +import Text from './ui/text'; import type { Menu } from 'pl-fe/components/dropdown-menu'; import type { Emoji as EmojiType } from 'pl-fe/features/emoji'; @@ -109,8 +112,79 @@ const messages = defineMessages({ addKnownLanguage: { id: 'status.add_known_language', defaultMessage: 'Do not auto-translate posts in {language}.' }, translate: { id: 'status.translate', defaultMessage: 'Translate' }, hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' }, + + favouriteInteractionPolicyHeader: { id: 'status.interaction_policy.favourite.header', defaultMessage: 'The author limits who can like this post.' }, + reblogInteractionPolicyHeader: { id: 'status.interaction_policy.reblog.header', defaultMessage: 'The author limits who can repost this post.' }, + replyInteractionPolicyHeader: { id: 'status.interaction_policy.reply.header', defaultMessage: 'The author limits who can reply to this post.' }, + + favouriteInteractionPolicyFollowers: { id: 'status.interaction_policy.favourite.followers_only', defaultMessage: 'Only users following the author can like.' }, + favouriteInteractionPolicyFollowing: { id: 'status.interaction_policy.favourite.following_only', defaultMessage: 'Only users followed by the author can like.' }, + favouriteInteractionPolicyMutuals: { id: 'status.interaction_policy.favourite.mutuals_only', defaultMessage: 'Only users mutually following the author can like.' }, + favouriteInteractionPolicyMentioned: { id: 'status.interaction_policy.favourite.mentioned_only', defaultMessage: 'Only users mentioned by the author can like.' }, + + reblogInteractionPolicyFollowers: { id: 'status.interaction_policy.reblog.followers_only', defaultMessage: 'Only users following the author can repost.' }, + reblogInteractionPolicyFollowing: { id: 'status.interaction_policy.reblog.following_only', defaultMessage: 'Only users followed by the author can repost.' }, + reblogInteractionPolicyMutuals: { id: 'status.interaction_policy.reblog.mutuals_only', defaultMessage: 'Only users mutually following the author can repost.' }, + reblogInteractionPolicyMentioned: { id: 'status.interaction_policy.reblog.mentioned_only', defaultMessage: 'Only users mentioned by the author can repost.' }, + + replyInteractionPolicyFollowers: { id: 'status.interaction_policy.reply.followers_only', defaultMessage: 'Only users following the author can reply.' }, + replyInteractionPolicyFollowing: { id: 'status.interaction_policy.reply.following_only', defaultMessage: 'Only users followed by the author can reply.' }, + replyInteractionPolicyMutuals: { id: 'status.interaction_policy.reply.mutuals_only', defaultMessage: 'Only users mutually following the author can reply.' }, + replyInteractionPolicyMentioned: { id: 'status.interaction_policy.reply.mentioned_only', defaultMessage: 'Only users mentioned by the author can reply.' }, + + favouriteApprovalRequired: { id: 'status.interaction_policy.favourite.approval_required', defaultMessage: 'The author needs to approve your like.' }, + reblogApprovalRequired: { id: 'status.interaction_policy.reblog.approval_required', defaultMessage: 'The author needs to approve your repost.' }, }); +interface IInteractionPopover { + type: 'favourite' | 'reblog' | 'reply'; + allowed: ReturnType['allowed']; +} + +const INTERACTION_POLICY_HEADERS = { + favourite: messages.favouriteInteractionPolicyHeader, + reblog: messages.reblogInteractionPolicyHeader, + reply: messages.replyInteractionPolicyHeader, +}; + +const INTERACTION_POLICY_DESCRIPTIONS = { + favourite: { + followers: messages.favouriteInteractionPolicyFollowers, + following: messages.favouriteInteractionPolicyFollowing, + mutuals: messages.favouriteInteractionPolicyMutuals, + mentioned: messages.favouriteInteractionPolicyMentioned, + }, + reblog: { + followers: messages.reblogInteractionPolicyFollowers, + following: messages.reblogInteractionPolicyFollowing, + mutuals: messages.reblogInteractionPolicyMutuals, + mentioned: messages.reblogInteractionPolicyMentioned, + }, + reply: { + followers: messages.replyInteractionPolicyFollowers, + following: messages.replyInteractionPolicyFollowing, + mutuals: messages.replyInteractionPolicyMutuals, + mentioned: messages.replyInteractionPolicyMentioned, + }, +}; + +const InteractionPopover: React.FC = ({ type, allowed }) => { + const intl = useIntl(); + + const allowedType = allowed?.includes('followers') ? 'followers' : allowed?.includes('following') ? 'following' : allowed?.includes('mutuals') ? 'mutuals' : 'mentioned'; + + return ( + + + {intl.formatMessage(INTERACTION_POLICY_HEADERS[type])} + + + {intl.formatMessage(INTERACTION_POLICY_DESCRIPTIONS[type][allowedType])} + + + ); +}; + interface IActionButton extends Pick { me: Me; onOpenUnauthorizedModal: (action?: UnauthorizedModalAction) => void; @@ -131,6 +205,7 @@ const ReplyButton: React.FC = ({ const dispatch = useAppDispatch(); const intl = useIntl(); + const canReply = useCanInteract(status, 'can_reply'); const { groupRelationship } = useGroupRelationship(status.group_id || undefined); let replyTitle; @@ -150,7 +225,7 @@ const ReplyButton: React.FC = ({ const handleReplyClick: React.MouseEventHandler = (e) => { if (me) { - dispatch(replyCompose(status, rebloggedBy)); + dispatch(replyCompose(status, rebloggedBy, canReply.approvalRequired || false)); } else { onOpenUnauthorizedModal('REPLY'); } @@ -168,6 +243,15 @@ const ReplyButton: React.FC = ({ /> ); + if (me && !canReply.canInteract) return ( + } + > + {replyButton} + + ); + return status.group ? ( = ({ const { boostModal } = useSettings(); const { openModal } = useModalsStore(); + const canReblog = useCanInteract(status, 'can_reblog'); let reblogIcon = require('@tabler/icons/outline/repeat.svg'); @@ -207,7 +292,9 @@ const ReblogButton: React.FC = ({ const handleReblogClick: React.EventHandler = e => { if (me) { - const modalReblog = () => dispatch(toggleReblog(status)); + const modalReblog = () => dispatch(toggleReblog(status)).then(() => { + if (canReblog.approvalRequired) toast.info(messages.reblogApprovalRequired); + }); if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { @@ -237,6 +324,15 @@ const ReblogButton: React.FC = ({ /> ); + if (me && !canReblog.canInteract) return ( + } + > + {reblogButton} + + ); + if (!features.quotePosts || !me) return reblogButton; const handleQuoteClick: React.EventHandler = (e) => { @@ -280,10 +376,13 @@ const FavouriteButton: React.FC = ({ const intl = useIntl(); const { openModal } = useModalsStore(); + const canFavourite = useCanInteract(status, 'can_favourite'); const handleFavouriteClick: React.EventHandler = (e) => { if (me) { - dispatch(toggleFavourite(status)); + dispatch(toggleFavourite(status)).then(() => { + if (canFavourite.approvalRequired) toast.info(messages.favouriteApprovalRequired); + }).catch(() => {}); } else { onOpenUnauthorizedModal('FAVOURITE'); } @@ -293,10 +392,10 @@ const FavouriteButton: React.FC = ({ openModal('FAVOURITES', { statusId: status.id }); } : undefined; - return ( + const favouriteButton = ( = ({ theme={statusActionButtonTheme} /> ); + + if (me && !canFavourite.canInteract) return ( + } + > + {favouriteButton} + + ); + return favouriteButton; }; const DislikeButton: React.FC = ({ @@ -409,7 +518,6 @@ interface IMenuButton extends IActionButton { const MenuButton: React.FC = ({ status, statusActionButtonTheme, - withLabels, me, expandable, fromBookmarks, @@ -420,10 +528,12 @@ const MenuButton: React.FC = ({ const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const { boostModal } = useSettings(); + const { statuses: statusesMeta, fetchTranslation, hideTranslation } = useStatusMetaStore(); + const targetLanguage = statusesMeta[status.id]?.targetLanguage; const { openModal } = useModalsStore(); const { group } = useGroup((status.group as Group)?.id as string); const deleteGroupStatus = useDeleteGroupStatus(group as Group, status.id); - const blockGroupMember = useBlockGroupMember(group as Group, status.account); + const { mutate: blockGroupMember } = useBlockGroupUserMutation(status.group?.id as string, status.account.id); const { getOrCreateChatByAccountId } = useChats(); const { groupRelationship } = useGroupRelationship(status.group_id || undefined); @@ -481,7 +591,7 @@ const MenuButton: React.FC = ({ }; const handleEditClick: React.EventHandler = () => { - if (status.event) dispatch(editEvent(status.id)); + if (status.event) history.push(`/@${status.account.acct}/events/${status.id}/edit`); else dispatch(editStatus(status.id)); }; @@ -515,7 +625,7 @@ const MenuButton: React.FC = ({ }; const handleMuteClick: React.EventHandler = (e) => { - dispatch(initMuteModal(status.account)); + openModal('MUTE', { accountId: status.account.id }); }; const handleBlockClick: React.EventHandler = (e) => { @@ -541,7 +651,7 @@ const MenuButton: React.FC = ({ }); }; - const handleOpenReactionsModal = (): void => { + const handleOpenReactionsModal = () => { openModal('REACTIONS', { statusId: status.id }); }; @@ -595,8 +705,8 @@ const MenuButton: React.FC = ({ message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: status.account.username }), confirm: intl.formatMessage(messages.groupBlockConfirm), onConfirm: () => { - blockGroupMember([status.account_id], { - onSuccess() { + blockGroupMember(undefined, { + onSuccess: () => { toast.success(intl.formatMessage(messages.blocked, { name: account?.acct })); }, }); @@ -609,10 +719,10 @@ const MenuButton: React.FC = ({ }; const handleTranslate = () => { - if (status.translation) { - dispatch(undoStatusTranslation(status.id)); + if (targetLanguage) { + hideTranslation(status.id); } else { - dispatch(translateStatus(status.id, intl.locale)); + fetchTranslation(status.id, intl.locale); } }; @@ -776,7 +886,7 @@ const MenuButton: React.FC = ({ } if (autoTranslating) { - if (status.translation) { + if (targetLanguage) { menu.push({ text: intl.formatMessage(messages.hideTranslation), action: handleTranslate, diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index c2dd7f896..ca249a227 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -1,18 +1,25 @@ import clsx from 'clsx'; -import React, { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useRef, useLayoutEffect, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import Icon from 'pl-fe/components/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import Emojify from 'pl-fe/features/emoji/emojify'; +import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container'; +import { useStatusTranslation } from 'pl-fe/queries/statuses/use-status-translation'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content'; import { getTextDirection } from '../utils/rtl'; +import HashtagsBar from './hashtags-bar'; import Markup from './markup'; -import { ParsedContent } from './parsed-content'; +import { parseContent } from './parsed-content'; import Poll from './polls/poll'; +import StatusMedia from './status-media'; +import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; +import TranslateButton from './translate-button'; import type { Sizes } from 'pl-fe/components/ui/text'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; @@ -23,11 +30,17 @@ interface IReadMoreButton { onClick: React.MouseEventHandler; quote?: boolean; poll?: boolean; + preview?: boolean; } /** Button to expand a truncated status (due to too much content) */ -const ReadMoreButton: React.FC = ({ onClick, quote, poll }) => ( -
+const ReadMoreButton: React.FC = ({ onClick, quote, poll, preview }) => ( +
= ({ onClick, quote, poll }) => 'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote, })} /> - + {!preview && ( + + )}
); @@ -48,7 +63,9 @@ interface IStatusContent { collapsable?: boolean; translatable?: boolean; textSize?: Sizes; - quote?: boolean; + isQuote?: boolean; + preview?: boolean; + withMedia?: boolean; } /** Renders the text content of a status */ @@ -58,7 +75,9 @@ const StatusContent: React.FC = React.memo(({ collapsable = false, translatable, textSize = 'md', - quote = false, + isQuote = false, + preview, + withMedia, }) => { const [collapsed, setCollapsed] = useState(false); const [onlyEmoji, setOnlyEmoji] = useState(false); @@ -67,12 +86,16 @@ const StatusContent: React.FC = React.memo(({ const node = useRef(null); const translationNode = useRef(null); + const { statuses: statusesMeta } = useStatusMetaStore(); + const statusMeta = statusesMeta[status.id] || {}; + const { data: translation } = useStatusTranslation(status.id, statusMeta.targetLanguage); + const maybeSetCollapsed = (): void => { if (!node.current) return; - if (collapsable && !collapsed) { + if ((collapsable || preview) && !collapsed) { // 20px * x lines (+ 2px padding at the top) - if (node.current.clientHeight > (quote ? 202 : 282)) { + if (node.current.clientHeight > (preview ? 82 : isQuote ? 202 : 282)) { setCollapsed(true); } } @@ -96,24 +119,39 @@ const StatusContent: React.FC = React.memo(({ setIsTranslationEqual(node.current?.innerText.trim().replace(/\s+/g, ' ') === translationNode.current?.innerText.trim().replace(/\s+/g, ' ')); }, [status.translation]); - const content = (status.content_map && status.currentLanguage) - ? status.content_map[status.currentLanguage] || status.content + const content = (status.content_map && statusMeta.currentLanguage) + ? status.content_map[statusMeta.currentLanguage] || status.content : status.content; - const translationContent = translatable && status.translation && !isTranslationEqual ? status.translation.content : null; + const { content: parsedContent, hashtags } = useMemo(() => parseContent({ + html: content, + mentions: status.mentions, + hasQuote: !!status.quote_id, + emojis: status.emojis, + }, true), [content]); + + const translationContent = translatable && translation ? translation.content : null; + + const { content: parsedTranslationContent } = useMemo(() => translationContent ? parseContent({ + html: translationContent, + mentions: status.mentions, + hasQuote: !!status.quote_id, + emojis: status.emojis, + }, true) : { content: null }, [translationContent]); const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none'; - const spoilerText = status.spoiler_text_map && status.currentLanguage - ? status.spoiler_text_map[status.currentLanguage] || status.spoiler_text + const spoilerText = status.spoiler_text_map && statusMeta.currentLanguage + ? status.spoiler_text_map[statusMeta.currentLanguage] || status.spoiler_text : status.spoiler_text; const direction = getTextDirection(status.search_index); const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', { 'cursor-pointer': onClick, 'overflow-hidden': collapsed, - 'max-h-[200px]': collapsed && !quote, - 'max-h-[120px]': collapsed && quote, + 'max-h-[200px]': collapsed && !isQuote && !preview, + 'max-h-[120px]': collapsed && isQuote, + 'max-h-[80px]': collapsed && preview, 'leading-normal big-emoji': onlyEmoji, }); @@ -129,6 +167,33 @@ const StatusContent: React.FC = React.memo(({ ); } + let quote; + + if (withMedia && status.quote_id) { + if ((status.quote_visible ?? true) === false) { + quote = ( +
+

+
+ ); + } else { + quote = ; + } + } + + const media = withMedia && ((quote || status.card || status.media_attachments.length > 0)) && ( + + {(status.media_attachments.length > 0 || (status.card && !quote)) && ( +
+ + +
+ )} + + {quote} +
+ ); + if (onClick) { if (content) { const body = ( @@ -141,7 +206,7 @@ const StatusContent: React.FC = React.memo(({ lang={status.language || undefined} size={textSize} > - + {parsedContent} ); @@ -161,7 +226,7 @@ const StatusContent: React.FC = React.memo(({ lang={status.language || undefined} size={textSize} > - + {parsedTranslationContent}
, @@ -174,11 +239,23 @@ const StatusContent: React.FC = React.memo(({ const hasPoll = !!status.poll_id; if (content && collapsed) { - output.push(); + output.push(); } if (status.poll_id) { - output.push(); + output.push(); + } + + if (translatable) { + output.push(); + } + + if (media) { + output.push(media); + } + + if (hashtags.length) { + output.push(); } return {output}; @@ -194,7 +271,7 @@ const StatusContent: React.FC = React.memo(({ lang={status.language || undefined} size={textSize} > - + {parsedContent} ); @@ -216,7 +293,7 @@ const StatusContent: React.FC = React.memo(({ direction={direction} size={textSize} > - + {parsedTranslationContent}
, @@ -226,12 +303,24 @@ const StatusContent: React.FC = React.memo(({ } if (collapsed) { - output.push( {}} key='read-more' quote={quote} />); + output.push( {}} key='read-more' quote={isQuote} preview={preview} />); } } if (status.poll_id) { - output.push(); + output.push(); + } + + if (translatable) { + output.push(); + } + + if (media) { + output.push(media); + } + + if (hashtags.length) { + output.push(); } return <>{output}; diff --git a/packages/pl-fe/src/components/status-hover-card.tsx b/packages/pl-fe/src/components/status-hover-card.tsx index 380a5f478..1f7ad2eb9 100644 --- a/packages/pl-fe/src/components/status-hover-card.tsx +++ b/packages/pl-fe/src/components/status-hover-card.tsx @@ -24,7 +24,7 @@ const StatusHoverCard: React.FC = ({ visible = true }) => { const { statusId, ref, closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardStore(); - const status = useAppSelector(state => state.statuses.get(statusId!)); + const status = useAppSelector(state => state.statuses[statusId!]); useEffect(() => { if (statusId && !status) { diff --git a/packages/pl-fe/src/components/status-language-picker.tsx b/packages/pl-fe/src/components/status-language-picker.tsx index 12af6318b..9db6409cc 100644 --- a/packages/pl-fe/src/components/status-language-picker.tsx +++ b/packages/pl-fe/src/components/status-language-picker.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { changeStatusLanguage } from 'pl-fe/actions/statuses'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Text from 'pl-fe/components/ui/text'; import { type Language, languages } from 'pl-fe/features/preferences'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import DropdownMenu from './dropdown-menu'; @@ -17,13 +16,16 @@ const messages = defineMessages({ }); interface IStatusLanguagePicker { - status: Pick; + status: Pick; showLabel?: boolean; } const StatusLanguagePicker: React.FC = ({ status, showLabel }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + + const { statuses, setStatusLanguage } = useStatusMetaStore(); + + const { currentLanguage } = statuses[status.id] || {}; if (!status.content_map || Object.keys(status.content_map).length < 2) return null; @@ -36,8 +38,8 @@ const StatusLanguagePicker: React.FC = ({ status, showLab ({ text: languages[language as Language] || language, - action: () => dispatch(changeStatusLanguage(status.id, language)), - active: language === status.currentLanguage, + action: () => setStatusLanguage(status.id, language), + active: language === currentLanguage, }))} >