build(android): add fdroid distribution flavor

This commit is contained in:
Tommaso Casaburi
2026-04-30 18:58:50 +07:00
parent ea03d04cd9
commit fdbb650baa
25 changed files with 259 additions and 75 deletions
@@ -62,6 +62,13 @@ const render = (children: React.ReactNode) => {
});
};
const settleLazyImports = async () => {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
};
const findButtonByText = (text: string) => Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text);
describe('InterfaceSettings', () => {
@@ -180,25 +187,27 @@ describe('InterfaceSettings', () => {
expect(localStorage.getItem(INTERFACE_LANGUAGE_STORAGE_KEY)).toBe('fr');
});
it('renders a check button when no app update is available', () => {
it('renders a check button when no app update is available', async () => {
render(createElement(InterfaceSettings));
await settleLazyImports();
expect(container.textContent).toContain('Update:');
expect(findButtonByText('Check')).toBeTruthy();
});
it('shows the checking status while an update check is in progress', () => {
it('shows the checking status while an update check is in progress', async () => {
useAppUpdateStore.setState({
isCheckingForUpdate: true,
});
render(createElement(InterfaceSettings));
await settleLazyImports();
expect(findButtonByText('Check')?.disabled).toBe(true);
expect(container.textContent).toContain('checking_for_updates');
});
it('renders a download button and release link when an app update is available', () => {
it('renders a download button and release link when an app update is available', async () => {
useAppUpdateStore.setState({
availableUpdate: {
runtime: 'web',
@@ -208,6 +217,7 @@ describe('InterfaceSettings', () => {
});
render(createElement(InterfaceSettings));
await settleLazyImports();
expect(findButtonByText('Download')).toBeTruthy();
const releaseLink = container.querySelector<HTMLAnchorElement>('a[href="https://github.com/bitsocialnet/5chan/releases/tag/v9.9.9"]');
@@ -217,6 +227,7 @@ describe('InterfaceSettings', () => {
it('checks for app updates when the check button is pressed', async () => {
render(createElement(InterfaceSettings));
await settleLazyImports();
const button = findButtonByText('Check');
expect(button).toBeTruthy();
@@ -242,6 +253,7 @@ describe('InterfaceSettings', () => {
});
render(createElement(InterfaceSettings));
await settleLazyImports();
const button = findButtonByText('Download');
expect(button).toBeTruthy();
@@ -255,7 +267,7 @@ describe('InterfaceSettings', () => {
expect(testState.alertMock).not.toHaveBeenCalled();
});
it('disables the update button while an app update is already being applied', () => {
it('disables the update button while an app update is already being applied', async () => {
useAppUpdateStore.setState({
availableUpdate: {
runtime: 'web',
@@ -266,6 +278,7 @@ describe('InterfaceSettings', () => {
});
render(createElement(InterfaceSettings));
await settleLazyImports();
expect(findButtonByText('Download')?.disabled).toBe(true);
});
@@ -283,6 +296,7 @@ describe('InterfaceSettings', () => {
});
render(createElement(InterfaceSettings));
await settleLazyImports();
const button = findButtonByText('Download');
expect(button).toBeTruthy();
@@ -0,0 +1,61 @@
import { useTranslation } from 'react-i18next';
import capitalize from 'lodash/capitalize';
import styles from './interface-settings.module.css';
import useAppUpdateStore from '../../../stores/use-app-update-store';
const UpdateButton = () => {
const { t } = useTranslation();
const availableUpdate = useAppUpdateStore((state) => state.availableUpdate);
const isApplyingUpdate = useAppUpdateStore((state) => state.isApplyingUpdate);
const isCheckingForUpdate = useAppUpdateStore((state) => state.isCheckingForUpdate);
const applyAppUpdate = useAppUpdateStore((state) => state.applyAppUpdate);
const refreshAvailableUpdate = useAppUpdateStore((state) => state.refreshAvailableUpdate);
const handleUpdateAction = async () => {
try {
if (availableUpdate) {
await applyAppUpdate();
return;
}
await refreshAvailableUpdate();
} catch (error) {
alert(String(error));
}
};
const buttonLabel = availableUpdate ? t('download') : t('check');
const isBusy = isApplyingUpdate || isCheckingForUpdate;
return (
<>
<button type='button' onClick={handleUpdateAction} disabled={isBusy}>
{capitalize(buttonLabel)}
</button>
{isCheckingForUpdate && (
<span className={styles.updateStatus} aria-live='polite'>
{t('checking_for_updates')}
</span>
)}
{!isCheckingForUpdate && availableUpdate && (
<span className={styles.updateStatus} aria-live='polite'>
{t('new_version_found')}:&nbsp;
<a href={availableUpdate.releaseUrl} target='_blank' rel='noopener noreferrer'>
v{availableUpdate.targetVersion}
</a>
</span>
)}
</>
);
};
const AppUpdateSetting = () => {
const { t } = useTranslation();
return (
<div className={styles.setting}>
{capitalize(t('update'))}: <UpdateButton />
</div>
);
};
export default AppUpdateSetting;
@@ -1,4 +1,4 @@
import { memo } from 'react';
import { lazy, memo, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './interface-settings.module.css';
import capitalize from 'lodash/capitalize';
@@ -7,52 +7,9 @@ import useFeedViewSettingsStore from '../../../stores/use-feed-view-settings-sto
import Version from '../../version';
import StyleSelector from '../../style-selector/style-selector';
import { INTERFACE_LANGUAGE_STORAGE_KEY, SUPPORTED_INTERFACE_LANGUAGES } from '../../../lib/constants';
import useAppUpdateStore from '../../../stores/use-app-update-store';
const UpdateButton = () => {
const { t } = useTranslation();
const availableUpdate = useAppUpdateStore((state) => state.availableUpdate);
const isApplyingUpdate = useAppUpdateStore((state) => state.isApplyingUpdate);
const isCheckingForUpdate = useAppUpdateStore((state) => state.isCheckingForUpdate);
const applyAppUpdate = useAppUpdateStore((state) => state.applyAppUpdate);
const refreshAvailableUpdate = useAppUpdateStore((state) => state.refreshAvailableUpdate);
const handleUpdateAction = async () => {
try {
if (availableUpdate) {
await applyAppUpdate();
return;
}
await refreshAvailableUpdate();
} catch (error) {
alert(String(error));
}
};
const buttonLabel = availableUpdate ? t('download') : t('check');
const isBusy = isApplyingUpdate || isCheckingForUpdate;
return (
<>
<button type='button' onClick={handleUpdateAction} disabled={isBusy}>
{capitalize(buttonLabel)}
</button>
{isCheckingForUpdate && (
<span className={styles.updateStatus} aria-live='polite'>
{t('checking_for_updates')}
</span>
)}
{!isCheckingForUpdate && availableUpdate && (
<span className={styles.updateStatus} aria-live='polite'>
{t('new_version_found')}:&nbsp;
<a href={availableUpdate.releaseUrl} target='_blank' rel='noopener noreferrer'>
v{availableUpdate.targetVersion}
</a>
</span>
)}
</>
);
};
const shouldRenderAppUpdateSetting = import.meta.env.VITE_APP_DISTRIBUTION !== 'fdroid';
const AppUpdateSetting = shouldRenderAppUpdateSetting ? lazy(() => import('./app-update-setting')) : null;
const InterfaceLanguage = () => {
const { i18n } = useTranslation();
@@ -87,9 +44,11 @@ const InterfaceSettings = () => {
<div className={styles.version}>
{capitalize(t('version'))}: <Version />
</div>
<div className={styles.setting}>
{capitalize(t('update'))}: <UpdateButton />
</div>
{AppUpdateSetting && (
<Suspense fallback={null}>
<AppUpdateSetting />
</Suspense>
)}
<div className={styles.setting}>
{capitalize(t('interface_language'))}: <InterfaceLanguage />
</div>