Merge branch main into fix-classnames
This commit is contained in:
parent
a3e52a15cc
commit
b7dcd4df8a
|
@ -18,5 +18,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scss.validate": false,
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, '<h1>Hello world</h1>');
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: FETCH_ABOUT_PAGE_REQUEST, slug: 'index' },
|
|
||||||
{ type: FETCH_ABOUT_PAGE_SUCCESS, slug: 'index', html: '<h1>Hello world</h1>' },
|
|
||||||
];
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -9,7 +9,6 @@ import { compareId } from 'soapbox/utils/comparators.ts';
|
||||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts';
|
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts';
|
||||||
import { unescapeHTML } from 'soapbox/utils/html.ts';
|
import { unescapeHTML } from 'soapbox/utils/html.ts';
|
||||||
import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification.ts';
|
import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification.ts';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static.ts';
|
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts.ts';
|
import { fetchRelationships } from './accounts.ts';
|
||||||
import { fetchGroupRelationships } from './groups.ts';
|
import { fetchGroupRelationships } from './groups.ts';
|
||||||
|
@ -120,7 +119,7 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<
|
||||||
icon: notification.account.avatar,
|
icon: notification.account.avatar,
|
||||||
tag: notification.id,
|
tag: notification.id,
|
||||||
data: {
|
data: {
|
||||||
url: joinPublicPath('/notifications'),
|
url: '/notifications',
|
||||||
},
|
},
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import KVStore from 'soapbox/storage/kv-store.ts';
|
||||||
import { removeVS16s } from 'soapbox/utils/emoji.ts';
|
import { removeVS16s } from 'soapbox/utils/emoji.ts';
|
||||||
import { getFeatures } from 'soapbox/utils/features.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 { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||||
|
@ -86,7 +86,7 @@ const loadSoapboxConfig = () =>
|
||||||
|
|
||||||
const fetchSoapboxJson = (host: string | null) =>
|
const fetchSoapboxJson = (host: string | null) =>
|
||||||
(dispatch: AppDispatch) =>
|
(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';
|
if (!isObject(data)) throw 'soapbox.json failed';
|
||||||
dispatch(importSoapboxConfig(data, host));
|
dispatch(importSoapboxConfig(data, host));
|
||||||
return data;
|
return data;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { HTTPError } from './HTTPError.ts';
|
import { HTTPError } from './HTTPError.ts';
|
||||||
|
import { MastodonResponse } from './MastodonResponse.ts';
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||||
|
@ -19,35 +20,35 @@ export class MastodonClient {
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(path: string, opts: Opts = {}): Promise<Response> {
|
async get(path: string, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('GET', path, undefined, opts);
|
return this.request('GET', path, undefined, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(path: string, data?: unknown, opts: Opts = {}): Promise<Response> {
|
async post(path: string, data?: unknown, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('POST', path, data, opts);
|
return this.request('POST', path, data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async put(path: string, data?: unknown, opts: Opts = {}): Promise<Response> {
|
async put(path: string, data?: unknown, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('PUT', path, data, opts);
|
return this.request('PUT', path, data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(path: string, opts: Opts = {}): Promise<Response> {
|
async delete(path: string, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('DELETE', path, undefined, opts);
|
return this.request('DELETE', path, undefined, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch(path: string, data: unknown, opts: Opts = {}): Promise<Response> {
|
async patch(path: string, data: unknown, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('PATCH', path, data, opts);
|
return this.request('PATCH', path, data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async head(path: string, opts: Opts = {}): Promise<Response> {
|
async head(path: string, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('HEAD', path, undefined, opts);
|
return this.request('HEAD', path, undefined, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async options(path: string, opts: Opts = {}): Promise<Response> {
|
async options(path: string, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
return this.request('OPTIONS', path, undefined, opts);
|
return this.request('OPTIONS', path, undefined, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise<Response> {
|
async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise<MastodonResponse> {
|
||||||
const url = new URL(path, this.baseUrl);
|
const url = new URL(path, this.baseUrl);
|
||||||
|
|
||||||
if (opts.searchParams) {
|
if (opts.searchParams) {
|
||||||
|
@ -89,7 +90,7 @@ export class MastodonClient {
|
||||||
throw new HTTPError(response, request);
|
throw new HTTPError(response, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return new MastodonResponse(response.body, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,8 +15,6 @@ const setupMock = (axios: AxiosInstance) => {
|
||||||
mocks.map(func => func(mock));
|
mocks.map(func => func(mock));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const staticClient = api.staticClient;
|
|
||||||
|
|
||||||
export const getLinks = (response: AxiosResponse): LinkHeader => {
|
export const getLinks = (response: AxiosResponse): LinkHeader => {
|
||||||
return new LinkHeader(response.headers?.link);
|
return new LinkHeader(response.headers?.link);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
* Stateful API client.
|
||||||
* Uses credentials from the Redux store if available.
|
* Uses credentials from the Redux store if available.
|
||||||
|
|
|
@ -3,15 +3,12 @@
|
||||||
* @module soapbox/build-config
|
* @module soapbox/build-config
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line import/extensions
|
|
||||||
import trim from 'lodash/trim.js';
|
|
||||||
// eslint-disable-next-line import/extensions
|
// eslint-disable-next-line import/extensions
|
||||||
import trimEnd from 'lodash/trimEnd.js';
|
import trimEnd from 'lodash/trimEnd.js';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
BACKEND_URL,
|
BACKEND_URL,
|
||||||
FE_SUBDIRECTORY,
|
|
||||||
FE_INSTANCE_SOURCE_DIR,
|
FE_INSTANCE_SOURCE_DIR,
|
||||||
SENTRY_DSN,
|
SENTRY_DSN,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
@ -24,14 +21,9 @@ const sanitizeURL = (url: string | undefined = ''): string => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeBasename = (path: string | undefined = ''): string => {
|
|
||||||
return `/${trim(path, '/')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
NODE_ENV: NODE_ENV || 'development',
|
NODE_ENV: NODE_ENV || 'development',
|
||||||
BACKEND_URL: sanitizeURL(BACKEND_URL),
|
BACKEND_URL: sanitizeURL(BACKEND_URL),
|
||||||
FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY),
|
|
||||||
FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance',
|
FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance',
|
||||||
SENTRY_DSN,
|
SENTRY_DSN,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { SoapboxEnv } from './build-config-compiletime.ts';
|
||||||
export const {
|
export const {
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
BACKEND_URL,
|
BACKEND_URL,
|
||||||
FE_SUBDIRECTORY,
|
|
||||||
FE_INSTANCE_SOURCE_DIR,
|
FE_INSTANCE_SOURCE_DIR,
|
||||||
SENTRY_DSN,
|
SENTRY_DSN,
|
||||||
} = import.meta.compileTime<SoapboxEnv>('./build-config-compiletime.ts');
|
} = import.meta.compileTime<SoapboxEnv>('./build-config-compiletime.ts');
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import unicodeMapping from 'soapbox/features/emoji/mapping.ts';
|
import unicodeMapping from 'soapbox/features/emoji/mapping.ts';
|
||||||
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static.ts';
|
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||||
className='emojione m-0 block'
|
className='emojione m-0 block'
|
||||||
alt={emoji}
|
alt={emoji}
|
||||||
title={title}
|
title={title}
|
||||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
src={`/packs/emoji/${filename}.svg`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (emojiMap.get(emoji as any)) {
|
} else if (emojiMap.get(emoji as any)) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { isCustomEmoji } from 'soapbox/features/emoji/index.ts';
|
import { isCustomEmoji } from 'soapbox/features/emoji/index.ts';
|
||||||
import unicodeMapping from 'soapbox/features/emoji/mapping.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';
|
import type { Emoji } from 'soapbox/features/emoji/index.ts';
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
|
url = `/packs/emoji/${mapping.unified}.svg`;
|
||||||
alt = emoji.native;
|
alt = emoji.native;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<ISentryFeedbackForm> = ({ eventId }) => {
|
|
||||||
const { account } = useOwnAccount();
|
|
||||||
|
|
||||||
const [feedback, setFeedback] = useState<string>();
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
||||||
const [isSubmitted, setIsSubmitted] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleFeedbackChange: React.ChangeEventHandler<HTMLTextAreaElement> = (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 (
|
|
||||||
<Text align='center'>
|
|
||||||
<FormattedMessage id='alert.unexpected.thanks' defaultMessage='Thanks for your feedback!' />
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form onSubmit={handleSubmitFeedback}>
|
|
||||||
<FormGroup>
|
|
||||||
<Textarea
|
|
||||||
value={feedback}
|
|
||||||
onChange={handleFeedbackChange}
|
|
||||||
placeholder='Anything you can tell us about what happened?'
|
|
||||||
disabled={isSubmitting}
|
|
||||||
autoGrow
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormActions>
|
|
||||||
<Button type='submit' className='mx-auto' disabled={!feedback || isSubmitting}>
|
|
||||||
<FormattedMessage id='alert.unexpected.submit_feedback' defaultMessage='Submit Feedback' />
|
|
||||||
</Button>
|
|
||||||
</FormActions>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SentryFeedbackForm;
|
|
|
@ -2,7 +2,6 @@ import { type ErrorInfo, useRef, useState } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { NODE_ENV } from 'soapbox/build-config.ts';
|
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.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 sourceCode from 'soapbox/utils/code.ts';
|
||||||
import { unregisterSW } from 'soapbox/utils/sw.ts';
|
import { unregisterSW } from 'soapbox/utils/sw.ts';
|
||||||
|
|
||||||
import SentryFeedbackForm from './sentry-feedback-form.tsx';
|
|
||||||
import SiteLogo from './site-logo.tsx';
|
import SiteLogo from './site-logo.tsx';
|
||||||
|
|
||||||
interface ISiteErrorBoundary {
|
interface ISiteErrorBoundary {
|
||||||
|
@ -22,16 +20,13 @@ interface ISiteErrorBoundary {
|
||||||
|
|
||||||
/** Application-level error boundary. Fills the whole screen. */
|
/** Application-level error boundary. Fills the whole screen. */
|
||||||
const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
const { links, sentryDsn } = useSoapboxConfig();
|
const { links } = useSoapboxConfig();
|
||||||
const textarea = useRef<HTMLTextAreaElement>(null);
|
const textarea = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const [error, setError] = useState<unknown>();
|
const [error, setError] = useState<unknown>();
|
||||||
const [componentStack, setComponentStack] = useState<string | null | undefined>();
|
const [componentStack, setComponentStack] = useState<string | null | undefined>();
|
||||||
const [browser, setBrowser] = useState<Bowser.Parser.Parser>();
|
const [browser, setBrowser] = useState<Bowser.Parser.Parser>();
|
||||||
const [sentryEventId, setSentryEventId] = useState<string>();
|
|
||||||
|
|
||||||
const sentryEnabled = Boolean(sentryDsn);
|
|
||||||
const isProduction = NODE_ENV === 'production';
|
|
||||||
const errorText = String(error) + componentStack;
|
const errorText = String(error) + componentStack;
|
||||||
|
|
||||||
const clearCookies: React.MouseEventHandler = (e) => {
|
const clearCookies: React.MouseEventHandler = (e) => {
|
||||||
|
@ -64,7 +59,6 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
ErrorBoundary: 'yes',
|
ErrorBoundary: 'yes',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((eventId) => setSentryEventId(eventId))
|
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
import('bowser')
|
import('bowser')
|
||||||
|
@ -127,12 +121,6 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||||
{(isProduction) ? (
|
|
||||||
(sentryEnabled && sentryEventId) && (
|
|
||||||
<SentryFeedbackForm eventId={sentryEventId} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{errorText && (
|
{errorText && (
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textarea}
|
ref={textarea}
|
||||||
|
@ -150,8 +138,6 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -3,13 +3,18 @@ import dotsIcon from '@tabler/icons/outline/dots.svg';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { closeModal, openModal } from 'soapbox/actions/modals.ts';
|
||||||
import EmojiComponent from 'soapbox/components/ui/emoji.tsx';
|
import EmojiComponent from 'soapbox/components/ui/emoji.tsx';
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import IconButton from 'soapbox/components/ui/icon-button.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 { useClickOutside } from 'soapbox/hooks/useClickOutside.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
import { userTouching } from 'soapbox/is-mobile.ts';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/features/emoji/index.ts';
|
import type { Emoji } from 'soapbox/features/emoji/index.ts';
|
||||||
|
|
||||||
|
@ -64,9 +69,11 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
offsetOptions,
|
offsetOptions,
|
||||||
all = true,
|
all = true,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const { allowedEmoji } = useSoapboxConfig();
|
||||||
const { customEmojiReacts } = useFeatures();
|
const { customEmojiReacts } = useFeatures();
|
||||||
|
const shortcodes = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const { x, y, strategy, refs, update } = useFloating<HTMLElement>({
|
const { x, y, strategy, refs, update } = useFloating<HTMLElement>({
|
||||||
|
@ -75,7 +82,18 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleExpand: React.MouseEventHandler = () => {
|
const handleExpand: React.MouseEventHandler = () => {
|
||||||
|
if (userTouching.matches) {
|
||||||
|
dispatch(openModal('EMOJI_PICKER', {
|
||||||
|
onPickEmoji: (emoji: Emoji) => {
|
||||||
|
handlePickEmoji(emoji);
|
||||||
|
dispatch(closeModal('EMOJI_PICKER'));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
onClose?.();
|
||||||
|
} else {
|
||||||
setExpanded(true);
|
setExpanded(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePickEmoji = (emoji: Emoji) => {
|
const handlePickEmoji = (emoji: Emoji) => {
|
||||||
|
@ -95,11 +113,19 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
useClickOutside(refs, () => {
|
useClickOutside(refs, () => {
|
||||||
if (onClose) {
|
onClose?.();
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recentEmojis = shortcodes.reduce<string[]>((results, shortcode) => {
|
||||||
|
const emoji = emojiData.emojis[shortcode]?.skins[0]?.native;
|
||||||
|
if (emoji) {
|
||||||
|
results.push(emoji);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const emojis = new Set([...recentEmojis, ...allowedEmoji]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('z-[101] transition-opacity duration-100', {
|
className={clsx('z-[101] transition-opacity duration-100', {
|
||||||
|
@ -125,7 +151,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
<HStack
|
<HStack
|
||||||
className={clsx('z-[999] flex w-max max-w-[100vw] flex-wrap space-x-3 rounded-full bg-white px-3 py-2.5 shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700')}
|
className={clsx('z-[999] flex w-max max-w-[100vw] flex-wrap space-x-3 rounded-full bg-white px-3 py-2.5 shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700')}
|
||||||
>
|
>
|
||||||
{Array.from(soapboxConfig.allowedEmoji).map((emoji, i) => (
|
{[...emojis].slice(0, 6).map((emoji, i) => (
|
||||||
<EmojiButton
|
<EmojiButton
|
||||||
key={i}
|
key={i}
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji.ts';
|
import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji.ts';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static.ts';
|
|
||||||
|
|
||||||
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
/** Unicode emoji character. */
|
/** Unicode emoji character. */
|
||||||
|
@ -23,7 +22,7 @@ const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||||
<img
|
<img
|
||||||
draggable='false'
|
draggable='false'
|
||||||
alt={alt || emoji}
|
alt={alt || emoji}
|
||||||
src={src || joinPublicPath(`packs/emoji/${filename}.svg`)}
|
src={src || `/packs/emoji/${filename}.svg`}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const themes = {
|
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',
|
transparent: 'bg-transparent p-0 shadow-none',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ const Modal = forwardRef<HTMLDivElement, IModal>(({
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-testid='modal'
|
data-testid='modal'
|
||||||
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl text-start align-middle text-gray-900 transition-all black:bg-black dark:bg-primary-900 dark:text-gray-100', widths[width], themes[theme])}
|
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl text-start align-middle transition-all', widths[width], themes[theme])}
|
||||||
>
|
>
|
||||||
<div className='w-full justify-between sm:flex sm:items-start'>
|
<div className='w-full justify-between sm:flex sm:items-start'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|
|
@ -45,9 +45,9 @@ export interface IEmojiPickerDropdown {
|
||||||
onPickEmoji?: (emoji: Emoji) => void;
|
onPickEmoji?: (emoji: Emoji) => void;
|
||||||
condensed?: boolean;
|
condensed?: boolean;
|
||||||
withCustom?: boolean;
|
withCustom?: boolean;
|
||||||
visible: boolean;
|
visible?: boolean;
|
||||||
setVisible: (value: boolean) => void;
|
setVisible?: (value: boolean) => void;
|
||||||
update: (() => any) | null;
|
update?: (() => any) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perLine = 8;
|
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
|
// Fixes render bug where popover has a delayed position update
|
||||||
const RenderAfter = ({ children, update }: any) => {
|
const RenderAfter: React.FC<IRenderAfter> = ({ children, update }) => {
|
||||||
const [nextTick, setNextTick] = useState(false);
|
const [nextTick, setNextTick] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -125,7 +130,7 @@ const RenderAfter = ({ children, update }: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
onPickEmoji, visible, setVisible, update, withCustom = true,
|
onPickEmoji, visible = true, setVisible, update, withCustom = true,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -136,7 +141,7 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
||||||
|
|
||||||
const handlePick = (emoji: any) => {
|
const handlePick = (emoji: any) => {
|
||||||
setVisible(false);
|
setVisible?.(false);
|
||||||
|
|
||||||
let pickedEmoji: Emoji;
|
let pickedEmoji: Emoji;
|
||||||
|
|
||||||
|
@ -213,10 +218,13 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
visible ? (
|
|
||||||
<RenderAfter update={update}>
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
<RenderAfter update={update ?? (() => {})}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -232,9 +240,8 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
skinTonePosition='search'
|
skinTonePosition='search'
|
||||||
previewPosition='none'
|
previewPosition='none'
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
</RenderAfter>
|
</RenderAfter>
|
||||||
) : null
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,12 @@ import spriteSheet from 'emoji-datasource/img/twitter/sheets/32.png';
|
||||||
import { Picker as EmojiPicker } from 'emoji-mart';
|
import { Picker as EmojiPicker } from 'emoji-mart';
|
||||||
import { useRef, useEffect } from 'react';
|
import { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
import { joinPublicPath } from 'soapbox/utils/static.ts';
|
|
||||||
|
|
||||||
import data from '../data.ts';
|
import data from '../data.ts';
|
||||||
|
|
||||||
const getSpritesheetURL = () => spriteSheet;
|
const getSpritesheetURL = () => spriteSheet;
|
||||||
|
|
||||||
const getImageURL = (set: string, name: string) => {
|
const getImageURL = (_set: string, name: string) => {
|
||||||
return joinPublicPath(`/packs/emoji/${name}.svg`);
|
return `/packs/emoji/${name}.svg`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Picker: React.FC<any> = (props) => {
|
const Picker: React.FC<any> = (props) => {
|
||||||
|
@ -21,7 +19,7 @@ const Picker: React.FC<any> = (props) => {
|
||||||
new EmojiPicker(input);
|
new EmojiPicker(input);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div ref={ref} />;
|
return <div className='flex justify-center' ref={ref} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Picker;
|
export default Picker;
|
||||||
|
|
|
@ -226,10 +226,10 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
|
||||||
|
|
||||||
let icon: string | undefined;
|
let icon: string | undefined;
|
||||||
|
|
||||||
if (isFollowing) {
|
if (blockedBy) {
|
||||||
icon = plusIcon;
|
|
||||||
} else if (blockedBy) {
|
|
||||||
icon = banIcon;
|
icon = banIcon;
|
||||||
|
} else if (!isFollowing) {
|
||||||
|
icon = plusIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
EditDomainModal,
|
EditDomainModal,
|
||||||
EditFederationModal,
|
EditFederationModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
|
EmojiPickerModal,
|
||||||
EventMapModal,
|
EventMapModal,
|
||||||
EventParticipantsModal,
|
EventParticipantsModal,
|
||||||
FamiliarFollowersModal,
|
FamiliarFollowersModal,
|
||||||
|
@ -72,6 +73,7 @@ const MODAL_COMPONENTS: Record<string, React.ExoticComponent<any>> = {
|
||||||
'EDIT_FEDERATION': EditFederationModal,
|
'EDIT_FEDERATION': EditFederationModal,
|
||||||
'EDIT_RULE': EditRuleModal,
|
'EDIT_RULE': EditRuleModal,
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
|
'EMOJI_PICKER': EmojiPickerModal,
|
||||||
'EVENT_MAP': EventMapModal,
|
'EVENT_MAP': EventMapModal,
|
||||||
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
||||||
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
||||||
|
|
|
@ -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<IEmojiPickerModal> = (props) => {
|
||||||
|
return (
|
||||||
|
<Modal className='flex' theme='transparent'>
|
||||||
|
<EmojiPickerDropdown {...props} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiPickerModal;
|
|
@ -2,6 +2,7 @@ import { lazy } from 'react';
|
||||||
|
|
||||||
export const AboutPage = lazy(() => import('soapbox/features/about/index.tsx'));
|
export const AboutPage = lazy(() => import('soapbox/features/about/index.tsx'));
|
||||||
export const EmojiPicker = lazy(() => import('soapbox/features/emoji/components/emoji-picker.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 Notifications = lazy(() => import('soapbox/features/notifications/index.tsx'));
|
||||||
export const LandingTimeline = lazy(() => import('soapbox/features/landing-timeline/index.tsx'));
|
export const LandingTimeline = lazy(() => import('soapbox/features/landing-timeline/index.tsx'));
|
||||||
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
|
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
|
||||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals.ts';
|
import { openModal } from 'soapbox/actions/modals.ts';
|
||||||
import * as BuildConfig from 'soapbox/build-config.ts';
|
|
||||||
import LoadingScreen from 'soapbox/components/loading-screen.tsx';
|
import LoadingScreen from 'soapbox/components/loading-screen.tsx';
|
||||||
import { ScrollContext } from 'soapbox/components/scroll-context.tsx';
|
import { ScrollContext } from 'soapbox/components/scroll-context.tsx';
|
||||||
import SiteErrorBoundary from 'soapbox/components/site-error-boundary.tsx';
|
import SiteErrorBoundary from 'soapbox/components/site-error-boundary.tsx';
|
||||||
|
@ -48,7 +47,7 @@ const SoapboxMount = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteErrorBoundary>
|
<SiteErrorBoundary>
|
||||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
<BrowserRouter>
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<ScrollContext>
|
<ScrollContext>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
|
@ -202,8 +202,6 @@
|
||||||
"alert.unexpected.links.support": "Support",
|
"alert.unexpected.links.support": "Support",
|
||||||
"alert.unexpected.message": "Something went wrong.",
|
"alert.unexpected.message": "Something went wrong.",
|
||||||
"alert.unexpected.return_home": "Return Home",
|
"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.add": "Create alias",
|
||||||
"aliases.account_label": "Old account:",
|
"aliases.account_label": "Old account:",
|
||||||
"aliases.aliases_list_delete": "Unlink alias",
|
"aliases.aliases_list_delete": "Unlink alias",
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { normalizeChatMessage } from 'soapbox/normalizers/index.ts';
|
||||||
import toast from 'soapbox/toast.tsx';
|
import toast from 'soapbox/toast.tsx';
|
||||||
import { ChatMessage } from 'soapbox/types/entities.ts';
|
import { ChatMessage } from 'soapbox/types/entities.ts';
|
||||||
import { reOrderChatListItems, updateChatMessage } from 'soapbox/utils/chats.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 { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries.ts';
|
||||||
|
|
||||||
import { queryClient } from './client.ts';
|
import { queryClient } from './client.ts';
|
||||||
|
@ -91,7 +90,7 @@ const useChatMessages = (chat: IChat) => {
|
||||||
const response = await api.get(uri);
|
const response = await api.get(uri);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const { next } = getPagination(response);
|
const { next } = response.pagination();
|
||||||
const hasMore = !!next;
|
const hasMore = !!next;
|
||||||
const result = data.map(normalizeChatMessage);
|
const result = data.map(normalizeChatMessage);
|
||||||
|
|
||||||
|
@ -144,7 +143,7 @@ const useChats = (search?: string) => {
|
||||||
});
|
});
|
||||||
const data: IChat[] = await response.json();
|
const data: IChat[] = await response.json();
|
||||||
|
|
||||||
const { next } = getPagination(response);
|
const { next } = response.pagination();
|
||||||
const hasMore = !!next;
|
const hasMore = !!next;
|
||||||
|
|
||||||
setUnreadChatsCount(Number(response.headers.get('x-unread-messages-count')) || sumBy(data, (chat) => chat.unread));
|
setUnreadChatsCount(Number(response.headers.get('x-unread-messages-count')) || sumBy(data, (chat) => chat.unread));
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||||
import { Account } from 'soapbox/types/entities.ts';
|
import { Account } from 'soapbox/types/entities.ts';
|
||||||
import { getPagination } from 'soapbox/utils/pagination.ts';
|
|
||||||
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries.ts';
|
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries.ts';
|
||||||
|
|
||||||
export default function useAccountSearch(q: string) {
|
export default function useAccountSearch(q: string) {
|
||||||
|
@ -21,7 +20,7 @@ export default function useAccountSearch(q: string) {
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const { next } = getPagination(response);
|
const { next } = response.pagination();
|
||||||
const hasMore = !!next;
|
const hasMore = !!next;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { fetchRelationships } from 'soapbox/actions/accounts.ts';
|
||||||
import { importFetchedAccounts } from 'soapbox/actions/importer/index.ts';
|
import { importFetchedAccounts } from 'soapbox/actions/importer/index.ts';
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { getPagination } from 'soapbox/utils/pagination.ts';
|
|
||||||
|
|
||||||
import { PaginatedResult, removePageItem } from '../utils/queries.ts';
|
import { PaginatedResult, removePageItem } from '../utils/queries.ts';
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ const useSuggestions = () => {
|
||||||
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
|
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
|
||||||
const endpoint = pageParam?.link || '/api/v2/suggestions';
|
const endpoint = pageParam?.link || '/api/v2/suggestions';
|
||||||
const response = await api.get(endpoint);
|
const response = await api.get(endpoint);
|
||||||
const { next } = getPagination(response);
|
const { next } = response.pagination();
|
||||||
const hasMore = !!next;
|
const hasMore = !!next;
|
||||||
|
|
||||||
const data: Suggestion[] = await response.json();
|
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 getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[]; link: string | undefined; hasMore: boolean }> => {
|
||||||
const link = pageParam?.link || '/api/v2/suggestions';
|
const link = pageParam?.link || '/api/v2/suggestions';
|
||||||
const response = await api.get(link);
|
const response = await api.get(link);
|
||||||
const { next } = getPagination(response);
|
const { next } = response.pagination();
|
||||||
const hasMore = !!next;
|
const hasMore = !!next;
|
||||||
|
|
||||||
const data: Suggestion[] = await response.json();
|
const data: Suggestion[] = await response.json();
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -6,7 +6,7 @@ import plugin from 'tailwindcss/plugin';
|
||||||
|
|
||||||
import { parseColorMatrix } from './tailwind/colors.ts';
|
import { parseColorMatrix } from './tailwind/colors.ts';
|
||||||
|
|
||||||
const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black &'));
|
const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black.black &'));
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],
|
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],
|
||||||
|
|
Loading…
Reference in New Issue