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 (
-
- );
-};
-
-export default SentryFeedbackForm;
\ No newline at end of file
diff --git a/src/components/site-error-boundary.tsx b/src/components/site-error-boundary.tsx
index 15186694b..b18dcba21 100644
--- a/src/components/site-error-boundary.tsx
+++ b/src/components/site-error-boundary.tsx
@@ -2,7 +2,6 @@ import { type ErrorInfo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { FormattedMessage } from 'react-intl';
-import { NODE_ENV } from 'soapbox/build-config.ts';
import HStack from 'soapbox/components/ui/hstack.tsx';
import Stack from 'soapbox/components/ui/stack.tsx';
import Text from 'soapbox/components/ui/text.tsx';
@@ -13,7 +12,6 @@ import KVStore from 'soapbox/storage/kv-store.ts';
import sourceCode from 'soapbox/utils/code.ts';
import { unregisterSW } from 'soapbox/utils/sw.ts';
-import SentryFeedbackForm from './sentry-feedback-form.tsx';
import SiteLogo from './site-logo.tsx';
interface ISiteErrorBoundary {
@@ -22,16 +20,13 @@ interface ISiteErrorBoundary {
/** Application-level error boundary. Fills the whole screen. */
const SiteErrorBoundary: React.FC = ({ children }) => {
- const { links, sentryDsn } = useSoapboxConfig();
+ const { links } = useSoapboxConfig();
const textarea = useRef(null);
const [error, setError] = useState();
const [componentStack, setComponentStack] = useState();
const [browser, setBrowser] = useState();
- const [sentryEventId, setSentryEventId] = useState();
- const sentryEnabled = Boolean(sentryDsn);
- const isProduction = NODE_ENV === 'production';
const errorText = String(error) + componentStack;
const clearCookies: React.MouseEventHandler = (e) => {
@@ -64,7 +59,6 @@ const SiteErrorBoundary: React.FC = ({ children }) => {
ErrorBoundary: 'yes',
},
})
- .then((eventId) => setSentryEventId(eventId))
.catch(console.error);
import('bowser')
@@ -127,30 +121,22 @@ const SiteErrorBoundary: React.FC = ({ children }) => {
- {(isProduction) ? (
- (sentryEnabled && sentryEventId) && (
-
- )
- ) : (
- <>
- {errorText && (
-
- )}
+ {errorText && (
+
+ )}
- {browser && (
-
-
- {browser.getBrowserName()} {browser.getBrowserVersion()}
-
- )}
- >
+ {browser && (
+
+
+ {browser.getBrowserName()} {browser.getBrowserVersion()}
+
)}
diff --git a/src/components/ui/emoji-selector.tsx b/src/components/ui/emoji-selector.tsx
index 77d0cea4c..ba06887bb 100644
--- a/src/components/ui/emoji-selector.tsx
+++ b/src/components/ui/emoji-selector.tsx
@@ -3,13 +3,18 @@ import dotsIcon from '@tabler/icons/outline/dots.svg';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
+import { closeModal, openModal } from 'soapbox/actions/modals.ts';
import EmojiComponent from 'soapbox/components/ui/emoji.tsx';
import HStack from 'soapbox/components/ui/hstack.tsx';
import IconButton from 'soapbox/components/ui/icon-button.tsx';
-import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
+import EmojiPickerDropdown, { getFrequentlyUsedEmojis } from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
+import emojiData from 'soapbox/features/emoji/data.ts';
+import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
+import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { useClickOutside } from 'soapbox/hooks/useClickOutside.ts';
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
+import { userTouching } from 'soapbox/is-mobile.ts';
import type { Emoji } from 'soapbox/features/emoji/index.ts';
@@ -64,9 +69,11 @@ const EmojiSelector: React.FC = ({
offsetOptions,
all = true,
}): JSX.Element => {
- const soapboxConfig = useSoapboxConfig();
+ const { allowedEmoji } = useSoapboxConfig();
const { customEmojiReacts } = useFeatures();
+ const shortcodes = useAppSelector((state) => getFrequentlyUsedEmojis(state));
+ const dispatch = useAppDispatch();
const [expanded, setExpanded] = useState(false);
const { x, y, strategy, refs, update } = useFloating({
@@ -75,7 +82,18 @@ const EmojiSelector: React.FC = ({
});
const handleExpand: React.MouseEventHandler = () => {
- setExpanded(true);
+ if (userTouching.matches) {
+ dispatch(openModal('EMOJI_PICKER', {
+ onPickEmoji: (emoji: Emoji) => {
+ handlePickEmoji(emoji);
+ dispatch(closeModal('EMOJI_PICKER'));
+ },
+ }));
+
+ onClose?.();
+ } else {
+ setExpanded(true);
+ }
};
const handlePickEmoji = (emoji: Emoji) => {
@@ -95,11 +113,19 @@ const EmojiSelector: React.FC = ({
}, [visible]);
useClickOutside(refs, () => {
- if (onClose) {
- onClose();
- }
+ onClose?.();
});
+ const recentEmojis = shortcodes.reduce((results, shortcode) => {
+ const emoji = emojiData.emojis[shortcode]?.skins[0]?.native;
+ if (emoji) {
+ results.push(emoji);
+ }
+ return results;
+ }, []);
+
+ const emojis = new Set([...recentEmojis, ...allowedEmoji]);
+
return (
= ({
- {Array.from(soapboxConfig.allowedEmoji).map((emoji, i) => (
+ {[...emojis].slice(0, 6).map((emoji, i) => (
{
/** Unicode emoji character. */
@@ -23,7 +22,7 @@ const Emoji: React.FC = (props): JSX.Element | null => {
);
diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx
index 595787da6..a7db84f99 100644
--- a/src/components/ui/modal.tsx
+++ b/src/components/ui/modal.tsx
@@ -16,7 +16,7 @@ const messages = defineMessages({
});
const themes = {
- normal: 'bg-white p-6 shadow-xl',
+ normal: 'bg-white black:bg-black dark:bg-primary-900 p-6 shadow-xl text-gray-900 dark:text-gray-100',
transparent: 'bg-transparent p-0 shadow-none',
};
@@ -105,7 +105,7 @@ const Modal = forwardRef(({
diff --git a/src/features/emoji/components/emoji-picker-dropdown.tsx b/src/features/emoji/components/emoji-picker-dropdown.tsx
index fcc9c05c3..bdae364b3 100644
--- a/src/features/emoji/components/emoji-picker-dropdown.tsx
+++ b/src/features/emoji/components/emoji-picker-dropdown.tsx
@@ -45,9 +45,9 @@ export interface IEmojiPickerDropdown {
onPickEmoji?: (emoji: Emoji) => void;
condensed?: boolean;
withCustom?: boolean;
- visible: boolean;
- setVisible: (value: boolean) => void;
- update: (() => any) | null;
+ visible?: boolean;
+ setVisible?: (value: boolean) => void;
+ update?: (() => any) | null;
}
const perLine = 8;
@@ -105,8 +105,13 @@ const getCustomEmojis = createSelector([
}
}));
+interface IRenderAfter {
+ children: React.ReactNode;
+ update: () => void;
+}
+
// Fixes render bug where popover has a delayed position update
-const RenderAfter = ({ children, update }: any) => {
+const RenderAfter: React.FC
= ({ children, update }) => {
const [nextTick, setNextTick] = useState(false);
useEffect(() => {
@@ -125,7 +130,7 @@ const RenderAfter = ({ children, update }: any) => {
};
const EmojiPickerDropdown: React.FC = ({
- onPickEmoji, visible, setVisible, update, withCustom = true,
+ onPickEmoji, visible = true, setVisible, update, withCustom = true,
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
@@ -136,7 +141,7 @@ const EmojiPickerDropdown: React.FC = ({
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
const handlePick = (emoji: any) => {
- setVisible(false);
+ setVisible?.(false);
let pickedEmoji: Emoji;
@@ -213,28 +218,30 @@ const EmojiPickerDropdown: React.FC = ({
document.body.style.overflow = '';
}, []);
+ if (!visible) {
+ return null;
+ }
+
return (
- visible ? (
-
-
-
-
+
+ {})}>
+
- ) : null
+
);
};
diff --git a/src/features/emoji/components/emoji-picker.tsx b/src/features/emoji/components/emoji-picker.tsx
index 74cdc5d0f..a6dc860ff 100644
--- a/src/features/emoji/components/emoji-picker.tsx
+++ b/src/features/emoji/components/emoji-picker.tsx
@@ -2,14 +2,12 @@ import spriteSheet from 'emoji-datasource/img/twitter/sheets/32.png';
import { Picker as EmojiPicker } from 'emoji-mart';
import { useRef, useEffect } from 'react';
-import { joinPublicPath } from 'soapbox/utils/static.ts';
-
import data from '../data.ts';
const getSpritesheetURL = () => spriteSheet;
-const getImageURL = (set: string, name: string) => {
- return joinPublicPath(`/packs/emoji/${name}.svg`);
+const getImageURL = (_set: string, name: string) => {
+ return `/packs/emoji/${name}.svg`;
};
const Picker: React.FC = (props) => {
@@ -21,7 +19,7 @@ const Picker: React.FC = (props) => {
new EmojiPicker(input);
}, []);
- return ;
+ return ;
};
export default Picker;
diff --git a/src/features/ui/components/action-button.tsx b/src/features/ui/components/action-button.tsx
index 8a2f6144c..c0c3e8650 100644
--- a/src/features/ui/components/action-button.tsx
+++ b/src/features/ui/components/action-button.tsx
@@ -226,10 +226,10 @@ const ActionButton: React.FC = ({ account, actionType, small }) =
let icon: string | undefined;
- if (isFollowing) {
- icon = plusIcon;
- } else if (blockedBy) {
+ if (blockedBy) {
icon = banIcon;
+ } else if (!isFollowing) {
+ icon = plusIcon;
}
return (
diff --git a/src/features/ui/components/modal-root.tsx b/src/features/ui/components/modal-root.tsx
index 0d8c71bd8..084517ea8 100644
--- a/src/features/ui/components/modal-root.tsx
+++ b/src/features/ui/components/modal-root.tsx
@@ -18,6 +18,7 @@ import {
EditDomainModal,
EditFederationModal,
EmbedModal,
+ EmojiPickerModal,
EventMapModal,
EventParticipantsModal,
FamiliarFollowersModal,
@@ -72,6 +73,7 @@ const MODAL_COMPONENTS: Record> = {
'EDIT_FEDERATION': EditFederationModal,
'EDIT_RULE': EditRuleModal,
'EMBED': EmbedModal,
+ 'EMOJI_PICKER': EmojiPickerModal,
'EVENT_MAP': EventMapModal,
'EVENT_PARTICIPANTS': EventParticipantsModal,
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
diff --git a/src/features/ui/components/modals/emoji-picker-modal.tsx b/src/features/ui/components/modals/emoji-picker-modal.tsx
new file mode 100644
index 000000000..e3cd91ccc
--- /dev/null
+++ b/src/features/ui/components/modals/emoji-picker-modal.tsx
@@ -0,0 +1,17 @@
+import Modal from 'soapbox/components/ui/modal.tsx';
+import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
+import { Emoji } from 'soapbox/features/emoji/index.ts';
+
+interface IEmojiPickerModal {
+ onPickEmoji?: (emoji: Emoji) => void;
+}
+
+export const EmojiPickerModal: React.FC = (props) => {
+ return (
+
+
+
+ );
+};
+
+export default EmojiPickerModal;
\ No newline at end of file
diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts
index 546e931ee..7b2a62ab5 100644
--- a/src/features/ui/util/async-components.ts
+++ b/src/features/ui/util/async-components.ts
@@ -2,6 +2,7 @@ import { lazy } from 'react';
export const AboutPage = lazy(() => import('soapbox/features/about/index.tsx'));
export const EmojiPicker = lazy(() => import('soapbox/features/emoji/components/emoji-picker.tsx'));
+export const EmojiPickerModal = lazy(() => import('soapbox/features/ui/components/modals/emoji-picker-modal.tsx'));
export const Notifications = lazy(() => import('soapbox/features/notifications/index.tsx'));
export const LandingTimeline = lazy(() => import('soapbox/features/landing-timeline/index.tsx'));
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx
index bf61583c3..eafccacae 100644
--- a/src/init/soapbox-mount.tsx
+++ b/src/init/soapbox-mount.tsx
@@ -4,7 +4,6 @@ import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { openModal } from 'soapbox/actions/modals.ts';
-import * as BuildConfig from 'soapbox/build-config.ts';
import LoadingScreen from 'soapbox/components/loading-screen.tsx';
import { ScrollContext } from 'soapbox/components/scroll-context.tsx';
import SiteErrorBoundary from 'soapbox/components/site-error-boundary.tsx';
@@ -48,7 +47,7 @@ const SoapboxMount = () => {
return (
-
+
diff --git a/src/locales/en.json b/src/locales/en.json
index 653fd7dca..d7d7b179d 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -202,8 +202,6 @@
"alert.unexpected.links.support": "Support",
"alert.unexpected.message": "Something went wrong.",
"alert.unexpected.return_home": "Return Home",
- "alert.unexpected.submit_feedback": "Submit Feedback",
- "alert.unexpected.thanks": "Thanks for your feedback!",
"aliases.account.add": "Create alias",
"aliases.account_label": "Old account:",
"aliases.aliases_list_delete": "Unlink alias",
diff --git a/src/queries/chats.ts b/src/queries/chats.ts
index a80c6f757..f353da79e 100644
--- a/src/queries/chats.ts
+++ b/src/queries/chats.ts
@@ -13,7 +13,6 @@ import { normalizeChatMessage } from 'soapbox/normalizers/index.ts';
import toast from 'soapbox/toast.tsx';
import { ChatMessage } from 'soapbox/types/entities.ts';
import { reOrderChatListItems, updateChatMessage } from 'soapbox/utils/chats.ts';
-import { getPagination } from 'soapbox/utils/pagination.ts';
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries.ts';
import { queryClient } from './client.ts';
@@ -91,7 +90,7 @@ const useChatMessages = (chat: IChat) => {
const response = await api.get(uri);
const data = await response.json();
- const { next } = getPagination(response);
+ const { next } = response.pagination();
const hasMore = !!next;
const result = data.map(normalizeChatMessage);
@@ -144,7 +143,7 @@ const useChats = (search?: string) => {
});
const data: IChat[] = await response.json();
- const { next } = getPagination(response);
+ const { next } = response.pagination();
const hasMore = !!next;
setUnreadChatsCount(Number(response.headers.get('x-unread-messages-count')) || sumBy(data, (chat) => chat.unread));
diff --git a/src/queries/search.ts b/src/queries/search.ts
index c23d84baa..38df39f0c 100644
--- a/src/queries/search.ts
+++ b/src/queries/search.ts
@@ -2,7 +2,6 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks/useApi.ts';
import { Account } from 'soapbox/types/entities.ts';
-import { getPagination } from 'soapbox/utils/pagination.ts';
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries.ts';
export default function useAccountSearch(q: string) {
@@ -21,7 +20,7 @@ export default function useAccountSearch(q: string) {
});
const data = await response.json();
- const { next } = getPagination(response);
+ const { next } = response.pagination();
const hasMore = !!next;
return {
diff --git a/src/queries/suggestions.ts b/src/queries/suggestions.ts
index 71076a86a..9268fe120 100644
--- a/src/queries/suggestions.ts
+++ b/src/queries/suggestions.ts
@@ -4,7 +4,6 @@ import { fetchRelationships } from 'soapbox/actions/accounts.ts';
import { importFetchedAccounts } from 'soapbox/actions/importer/index.ts';
import { useApi } from 'soapbox/hooks/useApi.ts';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
-import { getPagination } from 'soapbox/utils/pagination.ts';
import { PaginatedResult, removePageItem } from '../utils/queries.ts';
@@ -34,7 +33,7 @@ const useSuggestions = () => {
const getV2Suggestions = async (pageParam: PageParam): Promise> => {
const endpoint = pageParam?.link || '/api/v2/suggestions';
const response = await api.get(endpoint);
- const { next } = getPagination(response);
+ const { next } = response.pagination();
const hasMore = !!next;
const data: Suggestion[] = await response.json();
@@ -93,7 +92,7 @@ function useOnboardingSuggestions() {
const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[]; link: string | undefined; hasMore: boolean }> => {
const link = pageParam?.link || '/api/v2/suggestions';
const response = await api.get(link);
- const { next } = getPagination(response);
+ const { next } = response.pagination();
const hasMore = !!next;
const data: Suggestion[] = await response.json();
diff --git a/src/styles/application.scss b/src/styles/application.scss
index 329a4cdd9..d8c066c4a 100644
--- a/src/styles/application.scss
+++ b/src/styles/application.scss
@@ -8,4 +8,4 @@
@use 'components/compose-form';
@use 'components/status';
@use 'components/search';
-@use 'components/icon';
\ No newline at end of file
+@use 'components/icon';
diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts
deleted file mode 100644
index bf23f958f..000000000
--- a/src/utils/pagination.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import LinkHeader from 'http-link-header';
-
-interface Pagination {
- next?: string;
- prev?: string;
-}
-
-export function getPagination(response: Response): Pagination {
- const header = response.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/utils/static.ts b/src/utils/static.ts
deleted file mode 100644
index 266d975a1..000000000
--- a/src/utils/static.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Static: functions related to static files.
- * @module soapbox/utils/static
- */
-
-import { join } from 'path-browserify';
-
-import * as BuildConfig from 'soapbox/build-config.ts';
-
-/** Gets the path to a file with build configuration being considered. */
-export const joinPublicPath = (...paths: string[]): string => {
- return join(BuildConfig.FE_SUBDIRECTORY, ...paths);
-};
diff --git a/tailwind.config.ts b/tailwind.config.ts
index dfcd0472e..50c0d9e3d 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -6,7 +6,7 @@ import plugin from 'tailwindcss/plugin';
import { parseColorMatrix } from './tailwind/colors.ts';
-const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black &'));
+const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black.black &'));
const config: Config = {
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],