mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
build(android): add fdroid distribution flavor
This commit is contained in:
@@ -175,8 +175,8 @@ jobs:
|
||||
- name: Sync Capacitor Android project
|
||||
run: npx cap sync android
|
||||
|
||||
- name: Run Android unit tests and assemble debug APK
|
||||
run: cd android && ./gradlew testDebugUnitTest assembleDebug --stacktrace
|
||||
- name: Run Android unit tests and assemble debug APKs
|
||||
run: cd android && ./gradlew testGithubDebugUnitTest testFdroidDebugUnitTest assembleGithubDebug assembleFdroidDebug --stacktrace
|
||||
|
||||
package-linux:
|
||||
name: Package Linux (Ubuntu)
|
||||
|
||||
@@ -263,28 +263,30 @@ jobs:
|
||||
run: CI='' NODE_ENV=production yarn build
|
||||
- name: Set Android versionCode and versionName
|
||||
run: |
|
||||
sed -i "s/versionCode 1/versionCode $(git tag | wc -l)/" ./android/app/build.gradle
|
||||
sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle
|
||||
cat ./android/app/build.gradle
|
||||
VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||
VERSION_CODE=$(node -e "const [major = 0, minor = 0, patch = 0] = require('./package.json').version.replace(/^v/, '').split('-')[0].split('.').map(Number); console.log((major * 10000) + (minor * 100) + patch)")
|
||||
echo "APP_VERSION_NAME=${VERSION}" >> "$GITHUB_ENV"
|
||||
echo "APP_VERSION_CODE=${VERSION_CODE}" >> "$GITHUB_ENV"
|
||||
echo "Android versionName=${VERSION} versionCode=${VERSION_CODE}"
|
||||
- name: Sync Capacitor
|
||||
run: npx cap sync android
|
||||
- name: Build APK
|
||||
run: |
|
||||
for i in 1 2 3; do
|
||||
echo "gradlew assembleRelease attempt $i"
|
||||
(cd android && ./gradlew assembleRelease --stacktrace) && break
|
||||
echo "gradlew assembleGithubRelease attempt $i"
|
||||
(cd android && ./gradlew assembleGithubRelease -PAPP_VERSION_NAME="${APP_VERSION_NAME}" -PAPP_VERSION_CODE="${APP_VERSION_CODE}" --stacktrace) && break
|
||||
sleep 10
|
||||
[ "$i" = "3" ] && exit 1
|
||||
done
|
||||
- name: Optimize APK
|
||||
run: cd android/app/build/outputs/apk/release && zipalign 4 app-release-unsigned.apk app-release-unsigned-zip.apk
|
||||
run: cd android/app/build/outputs/apk/github/release && zipalign 4 app-github-release-unsigned.apk app-github-release-unsigned-zip.apk
|
||||
- name: Sign APK
|
||||
run: cd android/app/build/outputs/apk/release && apksigner sign --ks ../../../../../plebbit.keystore --ks-pass pass:${{ secrets.PLEBBIT_REACT_KEYSTORE_PASSWORD }} --ks-key-alias release --out app-release-signed.apk app-release-unsigned-zip.apk
|
||||
run: cd android/app/build/outputs/apk/github/release && apksigner sign --ks ../../../../../../plebbit.keystore --ks-pass pass:${{ secrets.PLEBBIT_REACT_KEYSTORE_PASSWORD }} --ks-key-alias release --out app-github-release-signed.apk app-github-release-unsigned-zip.apk
|
||||
- name: Stage release artifacts
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||
mv android/app/build/outputs/apk/release/app-release-signed.apk "release-assets/5chan-${VERSION}.apk"
|
||||
mv android/app/build/outputs/apk/github/release/app-github-release-signed.apk "release-assets/5chan-${VERSION}.apk"
|
||||
- name: List release assets
|
||||
run: ls -la release-assets
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
def resolvePackageVersionName() {
|
||||
def packageJsonFile = file("../../package.json")
|
||||
if (!packageJsonFile.exists()) {
|
||||
return "1.0"
|
||||
}
|
||||
|
||||
return new JsonSlurper().parse(packageJsonFile).version.toString()
|
||||
}
|
||||
|
||||
def versionNameToCode(String versionName) {
|
||||
def stableVersion = versionName.replaceFirst(/^v/, "").split("-")[0]
|
||||
def parts = stableVersion.tokenize(".").collect { part -> part.isInteger() ? part.toInteger() : 0 }
|
||||
|
||||
while (parts.size() < 3) {
|
||||
parts.add(0)
|
||||
}
|
||||
|
||||
return (parts[0] * 10000) + (parts[1] * 100) + parts[2]
|
||||
}
|
||||
|
||||
def resolvedVersionName = "${project.findProperty("APP_VERSION_NAME") ?: resolvePackageVersionName()}"
|
||||
def resolvedVersionCode = Integer.parseInt("${project.findProperty("APP_VERSION_CODE") ?: versionNameToCode(resolvedVersionName)}")
|
||||
|
||||
android {
|
||||
namespace "fivechan.android"
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
@@ -7,8 +32,8 @@ android {
|
||||
applicationId "fivechan.android"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode Integer.parseInt(project.findProperty("APP_VERSION_CODE") ?: "1")
|
||||
versionName "${project.findProperty("APP_VERSION_NAME") ?: "1.0"}"
|
||||
versionCode resolvedVersionCode
|
||||
versionName resolvedVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -16,6 +41,15 @@ android {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
github {
|
||||
dimension "distribution"
|
||||
}
|
||||
fdroid {
|
||||
dimension "distribution"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package fivechan.android;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.capacitorjs.plugins.statusbar.StatusBarPlugin;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
// Custom plugins must be registered before bridge initialization.
|
||||
registerPlugin(FileUploaderPlugin.class);
|
||||
registerPlugin(StatusBarPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
</manifest>
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
package fivechan.android;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import com.capacitorjs.plugins.statusbar.StatusBarPlugin;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
@@ -2,7 +2,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Draft metadata for https://gitlab.com/fdroid/fdroiddata.
|
||||
# Replace the commit field with the full v0.8.2 commit hash before opening the F-Droid merge request.
|
||||
Categories:
|
||||
- Internet
|
||||
License: GPL-3.0-or-later
|
||||
AuthorName: Bitsocial Forge
|
||||
WebSite: https://5chan.app
|
||||
SourceCode: https://github.com/bitsocialnet/5chan
|
||||
IssueTracker: https://github.com/bitsocialnet/5chan/issues
|
||||
Changelog: https://github.com/bitsocialnet/5chan/releases
|
||||
|
||||
AutoName: 5chan
|
||||
|
||||
RepoType: git
|
||||
Repo: https://github.com/bitsocialnet/5chan
|
||||
|
||||
Builds:
|
||||
- versionName: '0.8.2'
|
||||
versionCode: 802
|
||||
commit: TODO_REPLACE_WITH_V0_8_2_COMMIT_HASH
|
||||
subdir: android
|
||||
sudo:
|
||||
- curl -Lo node.tar.xz https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-x64.tar.xz
|
||||
- echo "22982235e1b71fa8850f82edd09cdae7e3f32df1764a9ec298c72d25ef2c164f node.tar.xz" | sha256sum -c -
|
||||
- tar xf node.tar.xz --strip-components=1 -C /usr/local/
|
||||
- corepack enable
|
||||
rm:
|
||||
- android/plebbit.keystore
|
||||
build:
|
||||
- cd .. && corepack yarn install --immutable && corepack yarn build:fdroid && corepack yarn cap sync android
|
||||
scandelete:
|
||||
- node_modules/
|
||||
- build/
|
||||
- android/app/build/
|
||||
- android/capacitor-cordova-android-plugins/build/
|
||||
gradle:
|
||||
- fdroid
|
||||
gradleprops:
|
||||
- APP_VERSION_NAME=0.8.2
|
||||
- APP_VERSION_CODE=802
|
||||
output: app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk
|
||||
|
||||
AutoUpdateMode: Version
|
||||
UpdateCheckMode: Tags
|
||||
CurrentVersion: '0.8.2'
|
||||
CurrentVersionCode: 802
|
||||
@@ -0,0 +1 @@
|
||||
Adds an F-Droid build without in-app APK updating and fixes Android version metadata.
|
||||
@@ -0,0 +1,5 @@
|
||||
5chan is a decentralized imageboard client for the Bitsocial protocol.
|
||||
|
||||
It keeps the familiar imageboard layout while letting anyone create and operate their own peer-to-peer communities. Boards are owned and moderated by their operators, and users can open any community directly by address.
|
||||
|
||||
The Android app supports browsing boards, posting, replying, media upload workflows, account management, themes, and the same directory-based discovery used by the web client.
|
||||
@@ -0,0 +1 @@
|
||||
Decentralized imageboard client for Bitsocial communities
|
||||
@@ -0,0 +1 @@
|
||||
5chan
|
||||
+2
-1
@@ -59,6 +59,7 @@
|
||||
"start": "node scripts/start-dev.js",
|
||||
"start:android-usb": "node scripts/start-android-usb.mjs",
|
||||
"build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build",
|
||||
"build:fdroid": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false VITE_APP_DISTRIBUTION=fdroid vite build",
|
||||
"build:preload": "vite build --config electron/vite.preload.config.js",
|
||||
"build-vercel": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build",
|
||||
"test": "vitest",
|
||||
@@ -93,7 +94,7 @@
|
||||
"type-check": "tsgo --noEmit",
|
||||
"prettier": "oxfmt src/**/*.{js,ts,tsx} electron/**/*.{js,mjs}",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
||||
"android:build": "corepack yarn build && npx cap sync android && npx cap run android",
|
||||
"android:build": "corepack yarn build && corepack yarn cap sync android && corepack yarn cap run android --flavor github",
|
||||
"i18n:update": "node scripts/update-translations.js",
|
||||
"i18n:update:dry": "node scripts/update-translations.js --dry",
|
||||
"doctor": "react-doctor . -y",
|
||||
|
||||
+18
-4
@@ -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')}:
|
||||
<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')}:
|
||||
<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>
|
||||
|
||||
Vendored
+2
@@ -3,6 +3,8 @@
|
||||
|
||||
declare interface ImportMetaEnv {
|
||||
readonly VITE_COMMIT_REF: string;
|
||||
readonly VITE_APP_VERSION?: string;
|
||||
readonly VITE_APP_DISTRIBUTION?: string;
|
||||
}
|
||||
|
||||
declare interface ImportMeta {
|
||||
|
||||
+6
-2
@@ -8,13 +8,13 @@ import './index.css';
|
||||
import './themes.css';
|
||||
import AppUpdateRegistration from './components/app-update-registration';
|
||||
import { App as CapacitorApp } from '@capacitor/app';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import { configureP2PBrowserPkcOptions } from './lib/p2p-browser-config';
|
||||
|
||||
// Only enable analytics on 5chan.app (Vercel deployment)
|
||||
// Exclude Electron (file:// or localhost), Capacitor/APK (capacitor:// or localhost), and IPFS (ipfs:// or different domain)
|
||||
const isVercelDeployment =
|
||||
typeof window !== 'undefined' && (window.location.hostname === '5chan.app' || window.location.hostname === 'www.5chan.app') && !window.isElectron;
|
||||
const shouldLoadAnalytics = import.meta.env.VITE_APP_DISTRIBUTION !== 'fdroid' && isVercelDeployment;
|
||||
const e2eStartHash = import.meta.env.VITE_E2E_START_HASH?.trim();
|
||||
const requestedE2EHarness = import.meta.env.DEV && typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('e2e') : null;
|
||||
|
||||
@@ -27,6 +27,7 @@ configureP2PBrowserPkcOptions();
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
const renderRoot = async () => {
|
||||
let e2eHarness: React.ComponentType | null = null;
|
||||
let Analytics: React.ComponentType | null = null;
|
||||
|
||||
if (requestedE2EHarness === 'thread-auto-update') {
|
||||
e2eHarness = (await import('./e2e/thread-auto-update-harness')).default;
|
||||
@@ -40,13 +41,16 @@ const renderRoot = async () => {
|
||||
}
|
||||
|
||||
const App = (await import('./app')).default;
|
||||
if (shouldLoadAnalytics) {
|
||||
Analytics = (await import('@vercel/analytics/react')).Analytics;
|
||||
}
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<AppUpdateRegistration />
|
||||
<App />
|
||||
{isVercelDeployment && <Analytics />}
|
||||
{Analytics && <Analytics />}
|
||||
</Router>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('app-update', () => {
|
||||
testState.electronDownloadAndInstallUpdateMock.mockReset();
|
||||
testState.electronGetPlatformMock.mockReset();
|
||||
testState.fetchMock.mockReset();
|
||||
vi.stubEnv('VITE_APP_VERSION', '0.8.1');
|
||||
vi.stubGlobal('fetch', testState.fetchMock);
|
||||
window.electronApi = undefined;
|
||||
Object.defineProperty(navigator, 'serviceWorker', {
|
||||
@@ -91,6 +92,27 @@ describe('app-update', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('disables update checks for F-Droid builds', async () => {
|
||||
vi.stubEnv('VITE_APP_DISTRIBUTION', 'fdroid');
|
||||
testState.capacitorPlatform = 'android';
|
||||
|
||||
const { applyAvailableAppUpdate, isAppUpdateEnabled, resolveAvailableAppUpdate } = await loadModule();
|
||||
|
||||
await expect(resolveAvailableAppUpdate()).resolves.toBeNull();
|
||||
await expect(
|
||||
applyAvailableAppUpdate({
|
||||
runtime: 'android',
|
||||
targetVersion: '9.9.9',
|
||||
assetName: '5chan-9.9.9.apk',
|
||||
downloadUrl: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9.apk',
|
||||
releaseUrl: 'https://github.com/bitsocialnet/5chan/releases/tag/v9.9.9',
|
||||
}),
|
||||
).rejects.toThrow('App updates are disabled for this build');
|
||||
expect(isAppUpdateEnabled).toBe(false);
|
||||
expect(testState.fetchMock).not.toHaveBeenCalled();
|
||||
expect(testState.androidDownloadAndInstallUpdateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects the matching electron release asset for the current desktop platform', async () => {
|
||||
window.electronApi = {
|
||||
isElectron: true,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const appDistribution = import.meta.env.VITE_APP_DISTRIBUTION?.trim().toLowerCase();
|
||||
const isAppUpdateEnabled = appDistribution !== 'fdroid';
|
||||
|
||||
export { isAppUpdateEnabled };
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import AppUpdater from '../plugins/app-updater';
|
||||
import { isAppUpdateEnabled } from './app-distribution';
|
||||
import { currentAppVersion } from './app-version';
|
||||
import { getDefaultReleaseUrl, getReleaseApiUrl, isAllowedDownloadUrl } from './app-update-config';
|
||||
|
||||
@@ -180,6 +180,10 @@ const fetchLatestReleaseUpdate = async (runtime: Extract<AppRuntime, 'electron'
|
||||
};
|
||||
|
||||
const resolveAvailableAppUpdate = async (): Promise<AvailableAppUpdate | null> => {
|
||||
if (!isAppUpdateEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtime = getAppRuntime();
|
||||
|
||||
if (runtime === 'web') {
|
||||
@@ -201,6 +205,10 @@ const resolveAvailableAppUpdate = async (): Promise<AvailableAppUpdate | null> =
|
||||
};
|
||||
|
||||
const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise<void> => {
|
||||
if (!isAppUpdateEnabled) {
|
||||
throw new Error('App updates are disabled for this build');
|
||||
}
|
||||
|
||||
if (update.runtime === 'web') {
|
||||
await refreshServiceWorkerRegistration().catch((error) => {
|
||||
console.error('Failed to refresh service worker registration', error);
|
||||
@@ -221,6 +229,7 @@ const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise<void
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: AppUpdater } = await import('../plugins/app-updater');
|
||||
await AppUpdater.downloadAndInstallUpdate({
|
||||
url: update.downloadUrl,
|
||||
fileName: update.assetName,
|
||||
@@ -228,4 +237,4 @@ const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise<void
|
||||
};
|
||||
|
||||
export type { AppRuntime, AvailableAppUpdate, NativeAppUpdateInfo, WebAppUpdateInfo };
|
||||
export { applyAvailableAppUpdate, fetchLatestStableVersion, getAppRuntime, isElectron, refreshServiceWorkerRegistration, resolveAvailableAppUpdate };
|
||||
export { applyAvailableAppUpdate, fetchLatestStableVersion, getAppRuntime, isAppUpdateEnabled, isElectron, refreshServiceWorkerRegistration, resolveAvailableAppUpdate };
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
const resolveCurrentAppVersion = (): string => {
|
||||
const configuredVersion = import.meta.env.VITE_APP_VERSION;
|
||||
|
||||
@@ -7,7 +5,7 @@ const resolveCurrentAppVersion = (): string => {
|
||||
return configuredVersion.trim();
|
||||
}
|
||||
|
||||
return packageJson.version;
|
||||
return '0.0.0';
|
||||
};
|
||||
|
||||
const currentAppVersion = resolveCurrentAppVersion();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
||||
const appVersion = `${process.env.VITE_APP_VERSION || packageVersion}`.trim() || packageVersion;
|
||||
process.env.VITE_APP_VERSION = appVersion;
|
||||
const publicBase = process.env.PUBLIC_URL || '/';
|
||||
const buildOutDir = 'build';
|
||||
const basePathPrefix = (() => {
|
||||
@@ -373,6 +374,7 @@ export default defineConfig({
|
||||
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
|
||||
'process.env.VITE_COMMIT_REF': JSON.stringify(process.env.COMMIT_REF),
|
||||
'process.version': JSON.stringify(''),
|
||||
global: 'globalThis',
|
||||
|
||||
Reference in New Issue
Block a user