Merge branch 'develop' of https://codeberg.org/mkljczk/pl-fe into develop
This commit is contained in:
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.1
|
||||
|
||||
### Added
|
||||
|
||||
- Missing ARIA attributes and other minor accessibility improvements.
|
||||
|
||||
### Changed
|
||||
|
||||
- Continued work on migrating styles from TailwindCSS.
|
||||
- Some files were moved to different locations.
|
||||
- Added a warning related to CORS configuration when signing in to an external instance.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed `console.log` statement accidentally left in the release code.
|
||||
- Requests without body do not get `Content-Type: application/json` appended by default, which fixes a bug with GoToSocial.
|
||||
- Last status in a thread view, if expanded, no longer gets bottom padding.
|
||||
|
||||
## v0.1.0
|
||||
|
||||
> This list includes changes made since the project forked from Soapbox in April 2024. It does not cover every UI change, consistency improvement, optimization, accessibility improvement, or backend compatibility update — maintaining such a list manually would be impractical.
|
||||
|
||||
@@ -10,12 +10,11 @@ import FaviconService from '@/utils/favicon-service';
|
||||
|
||||
FaviconService.initFaviconService();
|
||||
|
||||
interface IHelmet {
|
||||
interface IHeadTitle {
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Helmet: React.FC<IHelmet> = ({ title, children }) => {
|
||||
const HeadTitle: React.FC<IHeadTitle> = ({ title }) => {
|
||||
const instance = useInstance();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
const { data: awaitingApprovalCount = 0 } = usePendingUsersCount();
|
||||
@@ -45,12 +44,7 @@ const Helmet: React.FC<IHelmet> = ({ title, children }) => {
|
||||
? addCounter(`${title} | ${instance.title}`)
|
||||
: addCounter(instance.title);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{formattedTitle}</title>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
return <title>{formattedTitle}</title>;
|
||||
};
|
||||
|
||||
export { Helmet as default };
|
||||
export { HeadTitle as default };
|
||||
|
||||
@@ -613,6 +613,7 @@ const Audio: React.FC<IAudio> = (props) => {
|
||||
href={src}
|
||||
download
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Icon src={iconDownloadSimple} />
|
||||
</a>
|
||||
|
||||
@@ -621,6 +621,7 @@ const Video: React.FC<IVideo> = ({
|
||||
href={src}
|
||||
download
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Icon src={iconDownloadSimple} />
|
||||
</a>
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import Helmet from '@/components/helmet';
|
||||
import HeadTitle from '@/components/helmet';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
|
||||
import { Card, CardBody, CardHeader, CardTitle, type CardSizes } from './card';
|
||||
@@ -125,14 +125,13 @@ const Column: React.FC<IColumn> = (props): React.JSX.Element => {
|
||||
variant={transparent ? undefined : 'rounded'}
|
||||
className={clsx('⁂-column', className)}
|
||||
>
|
||||
<Helmet title={label}>
|
||||
{frontendConfig.appleAppId && (
|
||||
<meta
|
||||
name='apple-itunes-app'
|
||||
content={`app-id=${frontendConfig.appleAppId}, app-argument=${location.href}`}
|
||||
/>
|
||||
)}
|
||||
</Helmet>
|
||||
<HeadTitle title={label} />
|
||||
{frontendConfig.appleAppId && (
|
||||
<meta
|
||||
name='apple-itunes-app'
|
||||
content={`app-id=${frontendConfig.appleAppId}, app-argument=${location.href}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{withHeader && (
|
||||
<ColumnHeader
|
||||
|
||||
@@ -33,7 +33,7 @@ const IconButton = React.forwardRef(
|
||||
className={clsx('⁂-icon-button', `⁂-icon-button--${theme}`, className)}
|
||||
{...filteredProps}
|
||||
data-testid={filteredProps['data-testid'] ?? 'icon-button'}
|
||||
{...(props.href ? { target: '_blank' } : {})}
|
||||
{...(props.href ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
<SvgIcon src={src} className={iconClassName} aria-hidden />
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ const Thread = ({
|
||||
|
||||
const renderChildren = (list: Array<string>) =>
|
||||
list.map((id) => {
|
||||
if (id === status.id)
|
||||
if (id === status.id) {
|
||||
return (
|
||||
<div className={clsx({ 'pb-4': hasDescendants })} key={status.id}>
|
||||
{deleted ? (
|
||||
@@ -289,6 +289,7 @@ const Thread = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (id.endsWith('-tombstone')) {
|
||||
return renderTombstone(id);
|
||||
@@ -328,7 +329,7 @@ const Thread = ({
|
||||
[status.id],
|
||||
);
|
||||
|
||||
const hasDescendants = thread.length > statusIndex;
|
||||
const hasDescendants = thread.length > statusIndex + 1;
|
||||
|
||||
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import { useLocale, useLocaleDirection } from '@/hooks/use-locale';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { useThemeCss } from '@/hooks/use-theme-css';
|
||||
import { startSentry } from '@/sentry';
|
||||
import { useInstanceStore } from '@/stores/instance';
|
||||
import { useHasModals } from '@/stores/modals';
|
||||
import { useSettings } from '@/stores/settings';
|
||||
|
||||
const Helmet = React.lazy(() => import('@/components/helmet'));
|
||||
const HeadTitle = React.lazy(() => import('@/components/helmet'));
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
|
||||
@@ -30,6 +31,7 @@ const NicoliumHead = () => {
|
||||
const theme = useTheme();
|
||||
const [wcoVisible, setWcoVisible] = React.useState(false);
|
||||
const [wcoRight, setWcoRight] = React.useState(false);
|
||||
const instanceFetched = useInstanceStore((state) => state.fetched);
|
||||
|
||||
const withModals = useHasModals();
|
||||
|
||||
@@ -96,7 +98,7 @@ const NicoliumHead = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet />
|
||||
{instanceFetched && <HeadTitle />}
|
||||
<meta name='theme-color' content={color} />
|
||||
<InlineStyle>{`:root { ${themeCss} }`}</InlineStyle>
|
||||
{['dark', 'black'].includes(theme) && (
|
||||
|
||||
@@ -28,14 +28,20 @@ const NicoliumLoad: React.FC<INicoliumLoad> = ({ children }) => {
|
||||
/** Whether to display a loading indicator. */
|
||||
const showLoading = [me === null, me && !account, !isLoaded, localeLoading].some(Boolean);
|
||||
|
||||
console.log('[NicoliumLoad]', { me, account: !!account, isLoaded, localeLoading, showLoading });
|
||||
|
||||
// Load the user's locale
|
||||
useEffect(() => {
|
||||
console.log('[NicoliumLoad] Loading locale:', locale);
|
||||
MESSAGES[locale]()
|
||||
.then((messages) => {
|
||||
console.log('[NicoliumLoad] Locale loaded');
|
||||
setMessages(messages);
|
||||
setLocaleLoading(false);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((e) => {
|
||||
console.error('[NicoliumLoad] Locale failed:', e);
|
||||
});
|
||||
}, [locale]);
|
||||
|
||||
// Load initial data from the API
|
||||
@@ -43,19 +49,21 @@ const NicoliumLoad: React.FC<INicoliumLoad> = ({ children }) => {
|
||||
/** Load initial data from the backend */
|
||||
const loadInitial = async () => {
|
||||
checkIfStandalone();
|
||||
// Await for authenticated fetch
|
||||
console.log('[NicoliumLoad] fetchMe...');
|
||||
await fetchMe();
|
||||
// Await for feature detection
|
||||
console.log('[NicoliumLoad] fetchInstance...');
|
||||
await fetchInstance();
|
||||
// Await for configuration
|
||||
console.log('[NicoliumLoad] loadFrontendConfig...');
|
||||
await loadFrontendConfig();
|
||||
console.log('[NicoliumLoad] All loaded');
|
||||
};
|
||||
|
||||
loadInitial()
|
||||
.then(() => {
|
||||
setIsLoaded(true);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error('[NicoliumLoad] loadInitial failed:', e);
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1417,7 +1417,8 @@
|
||||
"login.otp_log_in.fail": "Invalid code, please try again.",
|
||||
"login.reset_password_hint": "Trouble logging in?",
|
||||
"login.sign_in": "Sign in",
|
||||
"login_external.errors.instance_fail": "The instance returned an error.",
|
||||
"login_external.errors.cors_fail": "Connection failed, likely due to CORS configuration. Is the instance configured to allow logins from other origins?",
|
||||
"login_external.errors.instance_fail": "The instance returned an error. Is the URL correct?",
|
||||
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
|
||||
"login_form.divider": "or",
|
||||
"login_form.external": "Sign in from remote instance",
|
||||
|
||||
@@ -13,7 +13,12 @@ const messages = defineMessages({
|
||||
instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' },
|
||||
instanceFailed: {
|
||||
id: 'login_external.errors.instance_fail',
|
||||
defaultMessage: 'The instance returned an error.',
|
||||
defaultMessage: 'The instance returned an error. Is the URL correct?',
|
||||
},
|
||||
corsFailed: {
|
||||
id: 'login_external.errors.cors_fail',
|
||||
defaultMessage:
|
||||
'Connection failed, likely due to CORS configuration. Is the instance configured to allow logins from other origins?',
|
||||
},
|
||||
networkFailed: {
|
||||
id: 'login_external.errors.network_fail',
|
||||
@@ -47,8 +52,10 @@ const ExternalLoginForm: React.FC = () => {
|
||||
console.error(error);
|
||||
const status = error.response?.status;
|
||||
|
||||
if (status) {
|
||||
if (status || !error.message) {
|
||||
toast.error(intl.formatMessage(messages.instanceFailed));
|
||||
} else if (error.message === 'NetworkError when attempting to fetch resource.') {
|
||||
toast.error(intl.formatMessage(messages.corsFailed));
|
||||
} else if (!status && ['Network request failed', 'Timeout'].includes(error.message)) {
|
||||
toast.error(intl.formatMessage(messages.networkFailed));
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const useTimeline = (
|
||||
|
||||
const poll = async () => {
|
||||
const sinceId =
|
||||
useTimelinesStore.getState().timelines[timelineId]?.queuedEntries[0]?.id ??
|
||||
useTimelinesStore.getState().timelines[timelineId]?.newestStatusId ??
|
||||
newestStatusId.current;
|
||||
if (!sinceId) return;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ interface TimelineData {
|
||||
isError: boolean;
|
||||
hasNextPage: boolean;
|
||||
oldestStatusId?: string;
|
||||
newestStatusId?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -202,6 +203,9 @@ const useTimelinesStore = create<State>()(
|
||||
}
|
||||
timeline.isPending = false;
|
||||
timeline.isFetching = false;
|
||||
if ((initialFetch || restoring) && statuses.length > 0) {
|
||||
timeline.newestStatusId = statuses[0].id;
|
||||
}
|
||||
if (typeof hasMore === 'boolean') {
|
||||
timeline.hasNextPage = hasMore;
|
||||
const oldestStatus = statuses.at(-1);
|
||||
@@ -220,9 +224,12 @@ const useTimelinesStore = create<State>()(
|
||||
)
|
||||
return;
|
||||
|
||||
if (!timeline.newestStatusId || timeline.newestStatusId.localeCompare(status.id) < 0) {
|
||||
timeline.newestStatusId = status.id;
|
||||
}
|
||||
timeline.queuedEntries.unshift(status);
|
||||
timeline.queuedCount += 1;
|
||||
timeline.queuedAccountIds.unshift(status.account.id);
|
||||
timeline.queuedAccountIds.unshift((status.reblog || status).account.id);
|
||||
});
|
||||
},
|
||||
deleteStatus: (statusId) => {
|
||||
@@ -269,6 +276,7 @@ const useTimelinesStore = create<State>()(
|
||||
|
||||
const processedEntries = processPage(timeline.queuedEntries);
|
||||
|
||||
timeline.newestStatusId = timeline.queuedEntries.toSorted().at(-1)!.id;
|
||||
timeline.entries.unshift(...processedEntries);
|
||||
timeline.queuedEntries = [];
|
||||
timeline.queuedCount = 0;
|
||||
|
||||
@@ -13,6 +13,10 @@ const compareId = (id1: string, id2: string) => {
|
||||
}
|
||||
if (id1.length === id2.length) {
|
||||
return id1 > id2 ? 1 : -1;
|
||||
} else if (id1.includes('/') && id2.includes('/')) {
|
||||
// Hollo notification IDs consist of a date, notification type and a UUID.
|
||||
// If both IDs start with a date, we can compare just the date part.
|
||||
return id1.split('/')[0] > id2.split('/')[0] ? 1 : -1;
|
||||
} else {
|
||||
return id1.length > id2.length ? 1 : -1;
|
||||
}
|
||||
|
||||
@@ -906,6 +906,7 @@ const getFeatures = (instance: Instance) => {
|
||||
v.software === AKKOMA,
|
||||
v.software === FIREFISH,
|
||||
v.software === GOTOSOCIAL,
|
||||
v.software === HOLLO,
|
||||
v.software === MASTODON,
|
||||
v.software === MITRA,
|
||||
v.software === PLEROMA,
|
||||
@@ -1390,6 +1391,7 @@ const getFeatures = (instance: Instance) => {
|
||||
polls: any([
|
||||
v.software === FIREFISH,
|
||||
v.software === GOTOSOCIAL,
|
||||
v.software === HOLLO,
|
||||
v.software === ICESHRIMP,
|
||||
v.software === ICESHRIMP_NET,
|
||||
v.software === MASTODON,
|
||||
@@ -1407,6 +1409,7 @@ const getFeatures = (instance: Instance) => {
|
||||
postLanguages: any([
|
||||
v.software === AKKOMA,
|
||||
v.software === GOTOSOCIAL,
|
||||
v.software === HOLLO,
|
||||
v.software === MASTODON,
|
||||
v.software === MITRA && gte(v.version, '3.23.0'),
|
||||
v.software === PLEROMA && gte(v.version, '2.9.0'),
|
||||
@@ -1473,6 +1476,7 @@ const getFeatures = (instance: Instance) => {
|
||||
v.software === FIREFISH,
|
||||
v.software === FRIENDICA,
|
||||
v.software === GOTOSOCIAL,
|
||||
v.software === HOLLO,
|
||||
v.software === ICESHRIMP,
|
||||
v.software === ICESHRIMP_NET,
|
||||
v.software === MASTODON,
|
||||
|
||||
@@ -99,7 +99,7 @@ function request<T = any>(
|
||||
params,
|
||||
onUploadProgress,
|
||||
signal,
|
||||
contentType = 'application/json',
|
||||
contentType,
|
||||
formData,
|
||||
idempotencyKey,
|
||||
}: RequestBody = {},
|
||||
@@ -113,7 +113,8 @@ function request<T = any>(
|
||||
else if (this.accessToken) headers.set('Authorization', `Bearer ${this.accessToken}`);
|
||||
else if (this.customAuthorizationToken)
|
||||
headers.set('Authorization', this.customAuthorizationToken);
|
||||
if (!formData) headers.set('Content-Type', contentType);
|
||||
if ((!formData && body) || contentType)
|
||||
headers.set('Content-Type', contentType || 'application/json');
|
||||
if (idempotencyKey) headers.set('Idempotency-Key', idempotencyKey);
|
||||
|
||||
body = body && formData ? serialize(body, { indices: true }) : JSON.stringify(body);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS: Array<string | RegExp> = [
|
||||
'/assets/default_header.webp', // GoToSocial
|
||||
'/headers/original/missing.png', // Mastodon
|
||||
'/headers/original/missing.png', // Hollo, Mastodon
|
||||
'/api/v1/accounts/identicon', // Mitra
|
||||
/\/static\/img\/missing\.[a-z0-9]+\.png$/, // NeoDB
|
||||
'/storage/headers/missing.png', // Pixelfed
|
||||
@@ -19,7 +19,7 @@ const isDefaultHeader = (url: string = '') =>
|
||||
/** Default avatar filenames from various backends */
|
||||
const DEFAULT_AVATARS: Array<string | RegExp> = [
|
||||
/\/assets\/default_avatars\/GoToSocial_icon[1-6]\.webp$/, // GoToSocial
|
||||
'/avatars/original/missing.png', // Mastodon
|
||||
'/avatars/original/missing.png', // Hollo, Mastodon
|
||||
'/api/v1/accounts/identicon', // Mitra
|
||||
'/s/img/avatar.svg', // NeoDB
|
||||
'/avatars/default.jpg', // Pixelfed
|
||||
|
||||
Reference in New Issue
Block a user