feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,216 @@
import * as React from 'react';
import { useState } from 'react';
import { connect } from 'react-redux';
import { Platform } from 'react-native';
import { AppState } from '../../utils/types';
import WarningBox, { WarningBoxAction, WarningBoxTarget } from './WarningBox';
import NavService from '@joplin/lib/services/NavService';
import { _ } from '@joplin/lib/locale';
import { showMissingMasterKeyMessage, showUnknownKeyFormatBanner } from '@joplin/lib/services/e2ee/utils';
import { localSyncInfoFromState, noteLockKeyConflictFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { NoteLockMigrationStatus } from '@joplin/lib/reducer';
import { confirmAndSkipNoteLockKeyMigration, notMigratedMessage, withSkippedCount } from '@joplin/lib/services/noteLock/NoteLockKeyMigration';
import { Dispatch } from 'redux';
import Setting from '@joplin/lib/models/Setting';
import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer';
import { substrWithEllipsis } from '@joplin/lib/string-utils';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { reg } from '@joplin/lib/registry';
const logger = Logger.create('WarningBanner');
interface Props {
themeId: number;
showMissingMasterKeyMessage: boolean;
showUnknownKeyFormatMessage: boolean;
hasDisabledSyncItems: boolean;
shouldUpgradeSyncTarget: boolean;
showShouldUpgradeSyncTargetMessage: boolean|undefined;
hasDisabledEncryptionItems: boolean;
mustUpgradeAppMessage: string;
syncTargetAppMinVersion?: string;
shareInvitations: ShareInvitation[];
processingShareInvitationResponse: boolean;
showInvalidJoplinCloudCredential: boolean;
noteLockKeyConflict: boolean;
showNoteLockKeyConflictMessage: boolean|undefined;
noteLockMigrationStatus: NoteLockMigrationStatus|null;
dispatch: Dispatch;
}
const androidGooglePlayUrl = 'https://play.google.com/store/apps/details?id=net.cozic.joplin';
const androidPreReleaseUrl = 'https://github.com/laurent22/joplin-android/tags';
const iosAppStoreUrl = 'https://apps.apple.com/app/id1315599797';
const fetchAndroidVersionIsPreRelease = async (version: string) => {
const response = await shim.fetch(`https://api.github.com/repos/laurent22/joplin-android/releases/tags/android-v${version}`);
if (!response.ok) return null;
const release = await response.json();
return !!release.prerelease;
};
const WarningBannerComponent: React.FC<Props> = props => {
const warningComps = [];
const [isAndroidTargetPreRelease, setIsAndroidTargetPreRelease] = useState<boolean|null>(null);
useAsyncEffect(async event => {
setIsAndroidTargetPreRelease(null);
if (Platform.OS !== 'android' || !props.mustUpgradeAppMessage || !props.syncTargetAppMinVersion) return;
try {
const isPreRelease = await fetchAndroidVersionIsPreRelease(props.syncTargetAppMinVersion);
if (!event.cancelled) setIsAndroidTargetPreRelease(isPreRelease);
} catch (error) {
logger.error('Could not load release metadata for version', props.syncTargetAppMinVersion, error);
}
}, [props.mustUpgradeAppMessage, props.syncTargetAppMinVersion]);
const renderWarningBox = (key: string, message: string, target: WarningBoxTarget, actions?: WarningBoxAction[]) => {
return <WarningBox
key={key}
themeId={props.themeId}
target={target}
message={message}
testID='warning-box'
actions={actions}
/>;
};
const renderMustUpgradeAppMessage = () => {
if (props.syncTargetAppMinVersion) {
const upgradeMessage = (message: string) => _(
'In order to synchronise, Please upgrade your application to version %s: %s',
props.syncTargetAppMinVersion,
message,
);
if (Platform.OS === 'android' && isAndroidTargetPreRelease !== null) {
if (isAndroidTargetPreRelease) {
return renderWarningBox(
'UpgradeApp',
upgradeMessage(_('Download it from the Joplin Android repository')),
{ url: androidPreReleaseUrl },
);
}
return renderWarningBox(
'UpgradeApp',
upgradeMessage(_('Update it from Google Play')),
{ url: androidGooglePlayUrl },
);
}
if (Platform.OS === 'ios') {
return renderWarningBox(
'UpgradeApp',
upgradeMessage(_('Update it from the App Store')),
{ url: iosAppStoreUrl },
);
}
return renderWarningBox('UpgradeApp', _('In order to synchronise, Please upgrade your application to version %s', props.syncTargetAppMinVersion), null);
}
return renderWarningBox('UpgradeApp', props.mustUpgradeAppMessage, null);
};
if (props.showMissingMasterKeyMessage) {
warningComps.push(renderWarningBox('missingDecryptionPassword', _('Press to set the decryption password.'), { screen: 'EncryptionConfig' }));
}
if (props.showUnknownKeyFormatMessage) {
warningComps.push(
renderWarningBox('unknownKeyFormat', _('One or more encryption keys are stored in an unknown format. Press for more info.'), { screen: 'EncryptionConfig' }),
);
}
if (props.hasDisabledSyncItems) {
warningComps.push(renderWarningBox('cannotSync', _('Some items cannot be synchronised. Press for more info.'), { screen: 'Status' }));
}
if (props.shouldUpgradeSyncTarget && props.showShouldUpgradeSyncTargetMessage !== false) {
warningComps.push(renderWarningBox('upgradeSync', _('The sync target needs to be upgraded. Press this banner to proceed.'), { screen: 'UpgradeSyncTarget' }));
}
if (props.mustUpgradeAppMessage) {
warningComps.push(renderMustUpgradeAppMessage());
}
if (props.hasDisabledEncryptionItems) {
warningComps.push(renderWarningBox('cannotDecrypt', _('Some items cannot be decrypted.'), { screen: 'Status' }));
}
const migration = props.noteLockMigrationStatus;
if (props.showNoteLockKeyConflictMessage !== false) {
if (migration?.running) {
warningComps.push(renderWarningBox('noteLockMigration', _('Re-encrypting your locked notes with the synced key. Migration is in progress...'), null));
} else if (migration?.failed || migration?.awaitingDecryption) {
warningComps.push(renderWarningBox('noteLockMigration', notMigratedMessage(migration), null, [
{ title: _('Retry'), onPress: () => void NavService.go('NoteLockMigration') },
{ title: _('Skip'), onPress: () => void confirmAndSkipNoteLockKeyMigration(props.dispatch) },
]));
} else if (migration) {
const onDismiss = () => props.dispatch({ type: 'NOTE_LOCK_MIGRATION_STATUS_SET', value: null });
warningComps.push(renderWarningBox('noteLockMigration', `${withSkippedCount(_('The migration of your locked notes has completed.'), migration)} ${_('Press to dismiss.')}`, { onPress: onDismiss }));
} else if (props.noteLockKeyConflict) {
warningComps.push(renderWarningBox('noteLockMigration', _('The sync target uses a different note lock key to the one on your device. Press to migrate your locked notes.'), { screen: 'NoteLockMigration' }));
}
}
if (props.showInvalidJoplinCloudCredential) {
const target = { screen: 'JoplinCloudLogin' };
warningComps.push(renderWarningBox('syncLogin', _('Your Joplin Cloud credentials are invalid, please login.'), target));
}
const shareInvitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting);
if (
!props.processingShareInvitationResponse
&& !!shareInvitation
) {
const invitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting);
const sharer = invitation.share.user;
warningComps.push(renderWarningBox(
'incomingShare',
_('%s (%s) would like to share a notebook with you.',
substrWithEllipsis(sharer?.full_name ?? 'Unknown', 0, 48),
substrWithEllipsis(sharer?.email ?? 'Unknown', 0, 52)),
{ screen: 'ShareManager' },
));
}
return warningComps;
};
const isSyncLoginRoute = (state: AppState) => {
const syncTargetId = state.settings['sync.target'];
const syncTarget = syncTargetId ? reg.syncTarget(syncTargetId) : null;
if (syncTarget) {
return state.route?.routeName === syncTarget.authRouteName();
}
return false;
};
const isEncryptionConfigRoute = (state: AppState) => {
return state.route?.routeName === 'EncryptionConfig';
};
export default connect((state: AppState) => {
const syncInfo = localSyncInfoFromState(state);
return {
themeId: state.settings.theme,
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedFolderId: state.selectedFolderId,
notesParentType: state.notesParentType,
showMissingMasterKeyMessage: !isEncryptionConfigRoute(state) && showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
showUnknownKeyFormatMessage: !isEncryptionConfigRoute(state) && showUnknownKeyFormatBanner(syncInfo),
hasDisabledSyncItems: state.hasDisabledSyncItems,
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
syncTargetAppMinVersion: state.mustUpgradeAppMinVersion,
shareInvitations: state.shareService.shareInvitations,
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && !isSyncLoginRoute(state) && state.mustAuthenticate,
noteLockKeyConflict: noteLockKeyConflictFromState(state),
noteLockMigrationStatus: state.noteLockMigrationStatus,
};
})(WarningBannerComponent);