diff --git a/.vscode/settings.json b/.vscode/settings.json index 79b4ed08d..3eef3018c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,10 @@ } ], "scss.validate": false, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "path-intellisense.extensionOnImport": true, + "javascript.preferences.importModuleSpecifierEnding": "js", + "javascript.preferences.importModuleSpecifier": "non-relative", + "typescript.preferences.importModuleSpecifierEnding": "js", + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/src/actions/about.test.ts b/src/actions/about.test.ts deleted file mode 100644 index 34e5240f7..000000000 --- a/src/actions/about.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { describe, expect, it } from 'vitest'; - -import { staticClient } from 'soapbox/api/index.ts'; -import { mockStore } from 'soapbox/jest/test-helpers.tsx'; - -import { - FETCH_ABOUT_PAGE_REQUEST, - FETCH_ABOUT_PAGE_SUCCESS, - FETCH_ABOUT_PAGE_FAIL, - fetchAboutPage, -} from './about.ts'; - -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({}); - - 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({}); - - return store.dispatch(fetchAboutPage('asdf')).catch(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); -}); diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index 6ecf8023b..03019abb4 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -9,7 +9,6 @@ import { compareId } from 'soapbox/utils/comparators.ts'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts'; import { unescapeHTML } from 'soapbox/utils/html.ts'; import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification.ts'; -import { joinPublicPath } from 'soapbox/utils/static.ts'; import { fetchRelationships } from './accounts.ts'; import { fetchGroupRelationships } from './groups.ts'; @@ -120,7 +119,7 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record< icon: notification.account.avatar, tag: notification.id, data: { - url: joinPublicPath('/notifications'), + url: '/notifications', }, }).catch(console.error); }).catch(console.error); diff --git a/src/actions/soapbox.ts b/src/actions/soapbox.ts index 31e7679b1..cee62ab4f 100644 --- a/src/actions/soapbox.ts +++ b/src/actions/soapbox.ts @@ -6,7 +6,7 @@ import KVStore from 'soapbox/storage/kv-store.ts'; import { removeVS16s } from 'soapbox/utils/emoji.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; -import api, { staticClient } from '../api/index.ts'; +import api from '../api/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity } from 'soapbox/types/entities.ts'; @@ -86,7 +86,7 @@ const loadSoapboxConfig = () => const fetchSoapboxJson = (host: string | null) => (dispatch: AppDispatch) => - staticClient.get('/instance/soapbox.json').then(({ data }) => { + fetch('/instance/soapbox.json').then((response) => response.json()).then((data) => { if (!isObject(data)) throw 'soapbox.json failed'; dispatch(importSoapboxConfig(data, host)); return data; diff --git a/src/api/MastodonClient.ts b/src/api/MastodonClient.ts index a0d084349..a41036b27 100644 --- a/src/api/MastodonClient.ts +++ b/src/api/MastodonClient.ts @@ -1,4 +1,5 @@ import { HTTPError } from './HTTPError.ts'; +import { MastodonResponse } from './MastodonResponse.ts'; interface Opts { searchParams?: URLSearchParams | Record; @@ -19,35 +20,35 @@ export class MastodonClient { this.accessToken = accessToken; } - async get(path: string, opts: Opts = {}): Promise { + async get(path: string, opts: Opts = {}): Promise { return this.request('GET', path, undefined, opts); } - async post(path: string, data?: unknown, opts: Opts = {}): Promise { + async post(path: string, data?: unknown, opts: Opts = {}): Promise { return this.request('POST', path, data, opts); } - async put(path: string, data?: unknown, opts: Opts = {}): Promise { + async put(path: string, data?: unknown, opts: Opts = {}): Promise { return this.request('PUT', path, data, opts); } - async delete(path: string, opts: Opts = {}): Promise { + async delete(path: string, opts: Opts = {}): Promise { return this.request('DELETE', path, undefined, opts); } - async patch(path: string, data: unknown, opts: Opts = {}): Promise { + async patch(path: string, data: unknown, opts: Opts = {}): Promise { return this.request('PATCH', path, data, opts); } - async head(path: string, opts: Opts = {}): Promise { + async head(path: string, opts: Opts = {}): Promise { return this.request('HEAD', path, undefined, opts); } - async options(path: string, opts: Opts = {}): Promise { + async options(path: string, opts: Opts = {}): Promise { return this.request('OPTIONS', path, undefined, opts); } - async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise { + async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise { const url = new URL(path, this.baseUrl); if (opts.searchParams) { @@ -89,7 +90,7 @@ export class MastodonClient { throw new HTTPError(response, request); } - return response; + return new MastodonResponse(response.body, response); } } \ No newline at end of file diff --git a/src/api/MastodonResponse.ts b/src/api/MastodonResponse.ts new file mode 100644 index 000000000..fb54008b0 --- /dev/null +++ b/src/api/MastodonResponse.ts @@ -0,0 +1,16 @@ +import LinkHeader from 'http-link-header'; + +export class MastodonResponse extends Response { + + /** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */ + pagination(): { prev?: string; next?: string } { + const header = this.headers.get('link'); + const links = header ? new LinkHeader(header) : undefined; + + return { + next: links?.refs.find((link) => link.rel === 'next')?.uri, + prev: links?.refs.find((link) => link.rel === 'prev')?.uri, + }; + } + +} diff --git a/src/api/__mocks__/index.ts b/src/api/__mocks__/index.ts index d0664bbe6..ff5fba2c2 100644 --- a/src/api/__mocks__/index.ts +++ b/src/api/__mocks__/index.ts @@ -15,8 +15,6 @@ const setupMock = (axios: AxiosInstance) => { mocks.map(func => func(mock)); }; -export const staticClient = api.staticClient; - export const getLinks = (response: AxiosResponse): LinkHeader => { return new LinkHeader(response.headers?.link); }; diff --git a/src/api/index.ts b/src/api/index.ts index 7a7e9aeaf..0898ab576 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -82,16 +82,6 @@ export const baseClient = ( }); }; -/** - * Dumb client for grabbing static files. - * It uses FE_SUBDIRECTORY and parses JSON if possible. - * No authorization is needed. - */ -export const staticClient = axios.create({ - baseURL: BuildConfig.FE_SUBDIRECTORY, - transformResponse: [maybeParseJSON], -}); - /** * Stateful API client. * Uses credentials from the Redux store if available. diff --git a/src/build-config-compiletime.ts b/src/build-config-compiletime.ts index e4ccfb943..8c74312b2 100644 --- a/src/build-config-compiletime.ts +++ b/src/build-config-compiletime.ts @@ -3,15 +3,12 @@ * @module soapbox/build-config */ -// eslint-disable-next-line import/extensions -import trim from 'lodash/trim.js'; // eslint-disable-next-line import/extensions import trimEnd from 'lodash/trimEnd.js'; const { NODE_ENV, BACKEND_URL, - FE_SUBDIRECTORY, FE_INSTANCE_SOURCE_DIR, SENTRY_DSN, } = process.env; @@ -24,14 +21,9 @@ const sanitizeURL = (url: string | undefined = ''): string => { } }; -const sanitizeBasename = (path: string | undefined = ''): string => { - return `/${trim(path, '/')}`; -}; - const env = { NODE_ENV: NODE_ENV || 'development', BACKEND_URL: sanitizeURL(BACKEND_URL), - FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY), FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance', SENTRY_DSN, }; diff --git a/src/build-config.ts b/src/build-config.ts index d8799de7e..340a691e6 100644 --- a/src/build-config.ts +++ b/src/build-config.ts @@ -3,7 +3,6 @@ import type { SoapboxEnv } from './build-config-compiletime.ts'; export const { NODE_ENV, BACKEND_URL, - FE_SUBDIRECTORY, FE_INSTANCE_SOURCE_DIR, SENTRY_DSN, } = import.meta.compileTime('./build-config-compiletime.ts'); diff --git a/src/components/announcements/emoji.tsx b/src/components/announcements/emoji.tsx index 927278484..f9994ec47 100644 --- a/src/components/announcements/emoji.tsx +++ b/src/components/announcements/emoji.tsx @@ -1,6 +1,5 @@ import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; -import { joinPublicPath } from 'soapbox/utils/static.ts'; import type { Map as ImmutableMap } from 'immutable'; @@ -25,7 +24,7 @@ const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { className='emojione m-0 block' alt={emoji} title={title} - src={joinPublicPath(`packs/emoji/${filename}.svg`)} + src={`/packs/emoji/${filename}.svg`} /> ); } else if (emojiMap.get(emoji as any)) { diff --git a/src/components/autosuggest-emoji.tsx b/src/components/autosuggest-emoji.tsx index 8337456f7..fb4a4b4fc 100644 --- a/src/components/autosuggest-emoji.tsx +++ b/src/components/autosuggest-emoji.tsx @@ -1,6 +1,5 @@ import { isCustomEmoji } from 'soapbox/features/emoji/index.ts'; import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; -import { joinPublicPath } from 'soapbox/utils/static.ts'; import type { Emoji } from 'soapbox/features/emoji/index.ts'; @@ -21,7 +20,7 @@ const AutosuggestEmoji: React.FC = ({ emoji }) => { return null; } - url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`); + url = `/packs/emoji/${mapping.unified}.svg`; alt = emoji.native; } diff --git a/src/components/sentry-feedback-form.tsx b/src/components/sentry-feedback-form.tsx deleted file mode 100644 index ea1eb1db9..000000000 --- a/src/components/sentry-feedback-form.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; - -import Button from 'soapbox/components/ui/button.tsx'; -import FormActions from 'soapbox/components/ui/form-actions.tsx'; -import FormGroup from 'soapbox/components/ui/form-group.tsx'; -import Form from 'soapbox/components/ui/form.tsx'; -import Text from 'soapbox/components/ui/text.tsx'; -import Textarea from 'soapbox/components/ui/textarea.tsx'; -import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -import { captureSentryFeedback } from 'soapbox/sentry.ts'; - -interface ISentryFeedbackForm { - eventId: string; -} - -/** Accept feedback for the given Sentry event. */ -const SentryFeedbackForm: React.FC = ({ eventId }) => { - const { account } = useOwnAccount(); - - const [feedback, setFeedback] = useState(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isSubmitted, setIsSubmitted] = useState(false); - - const handleFeedbackChange: React.ChangeEventHandler = (e) => { - setFeedback(e.target.value); - }; - - const handleSubmitFeedback: React.FormEventHandler = async (_e) => { - if (!feedback || !eventId) return; - setIsSubmitting(true); - - await captureSentryFeedback({ - name: account?.acct, - associatedEventId: eventId, - message: feedback, - }).catch(console.error); - - setIsSubmitted(true); - }; - - if (isSubmitted) { - return ( - - - - ); - } - - return ( -
- -