文件
mynode/packages/app-desktop/gui/WarningBanner.tsx
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

433 行
15 KiB
TypeScript

import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import * as React from 'react';
import { Dispatch } from 'redux';
import checkForUpdates, { isReleaseVersion } from '../checkForUpdates';
import bridge from '../services/bridge';
import restart from '../services/restart';
import invitationRespond from '@joplin/lib/services/share/invitationRespond';
import { _ } from '@joplin/lib/locale';
import { ShareInvitation } from '@joplin/lib/services/share/reducer';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { useEffect, useRef, useState } from 'react';
import Logger from '@joplin/utils/Logger';
import { AppState } from '../app.reducer';
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { showMissingMasterKeyMessage, showUnknownKeyFormatBanner } from '@joplin/lib/services/e2ee/utils';
import { noteLockKeyConflictFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { NoteLockMigrationStatus } from '@joplin/lib/reducer';
import { confirmAndSkipNoteLockKeyMigration, notMigratedMessage, withSkippedCount } from '@joplin/lib/services/noteLock/NoteLockKeyMigration';
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import { connect } from 'react-redux';
const logger = Logger.create('WarningBanner');
interface Props {
dispatch: Dispatch;
isSafeMode: boolean;
hasMissingSyncCredentials: boolean;
shouldUpgradeSyncTarget: boolean;
hasDisabledEncryptionItems: boolean;
showNeedUpgradingMasterKeyMessage: boolean;
showShouldReencryptMessage: boolean;
processingShareInvitationResponse: boolean;
shareInvitations: ShareInvitation[];
hasDisabledSyncItems: boolean;
showUnknownKeyFormatMessage: boolean;
showMissingMasterKeyMessage: boolean;
showNoteLockKeyConflictMessage: boolean;
noteLockMigrationStatus: NoteLockMigrationStatus|null;
mustUpgradeAppMessage: string;
syncTargetAppMinVersion: string;
shouldSwitchToAppleSiliconVersion: boolean;
showInvalidJoplinCloudCredential: boolean;
onShow: ()=> void;
onHide: ()=> void;
height: number;
}
const WarningBanner: React.FC<Props> = props => {
const propsRef = useRef(props);
propsRef.current = props;
const renderNotificationMessage = (
message: string, callForAction: string = null, callForActionHandler: ()=> void = null, callForAction2: string = null, callForActionHandler2: ()=> void = null,
) => {
return <NotificationMessage
message={message}
callForAction={callForAction}
callForActionHandler={callForActionHandler}
callForAction2={callForAction2}
callForActionHandler2={callForActionHandler2}
/>;
};
const onViewStatusScreen = () => {
props.dispatch({
type: 'NAV_GO',
routeName: 'Status',
});
};
const onViewEncryptionConfigScreen = () => {
props.dispatch({
type: 'NAV_GO',
routeName: 'Config',
props: {
defaultSection: 'encryption',
},
});
};
const onMigrateLockedNotes = () => {
props.dispatch({
type: 'DIALOG_OPEN',
name: 'noteLockMigration',
});
};
const onDismissNoteLockMigrationStatus = () => {
props.dispatch({
type: 'NOTE_LOCK_MIGRATION_STATUS_SET',
value: null,
});
};
const onViewJoplinCloudLoginScreen = () => {
props.dispatch({
type: 'NAV_GO',
routeName: 'JoplinCloudLogin',
});
};
const onDisableSync = () => {
Setting.setValue('sync.target', null);
};
const onViewSyncSettingsScreen = () => {
props.dispatch({
type: 'NAV_GO',
routeName: 'Config',
props: {
defaultSection: 'sync',
},
});
};
const onDownloadAppleSiliconVersion = () => {
// The website should redirect to the correct version
shim.openUrl('https://joplinapp.org/download/');
};
const onCheckForUpdates = () => {
void checkForUpdates(false, bridge().mainWindow(), { includePreReleases: false });
};
const onRestartAndUpgrade = async () => {
Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO);
await Setting.saveAll();
await restart();
};
const onDisableSafeModeAndRestart = async () => {
Setting.setValue('isSafeMode', false);
await Setting.saveAll();
await restart();
};
const onInvitationRespond = async (shareUserId: string, folderId: string, masterKey: MasterKeyEntity, accept: boolean) => {
await invitationRespond(shareUserId, folderId, masterKey, accept);
};
const showShareInvitationNotification = () => {
if (props.processingShareInvitationResponse) return false;
return !!props.shareInvitations.find(i => i.status === 0);
};
const appMinVersionState = useAppMinVersionState({ appMinVersion: props.syncTargetAppMinVersion, mustUpgradeAppMessage: props.mustUpgradeAppMessage });
let msg = null;
if (props.isSafeMode) {
msg = renderNotificationMessage(
_('Safe mode is currently active. Note rendering and all plugins are temporarily disabled.'),
_('Disable safe mode and restart'),
onDisableSafeModeAndRestart,
);
} else if (props.hasMissingSyncCredentials) {
msg = renderNotificationMessage(
_('The synchronisation password is missing.'),
_('Set the password'),
onViewSyncSettingsScreen,
);
} else if (props.noteLockMigrationStatus?.running) {
msg = renderNotificationMessage(_('Re-encrypting your locked notes with the synced key. Migration is in progress...'));
} else if (props.noteLockMigrationStatus?.failed || props.noteLockMigrationStatus?.awaitingDecryption) {
msg = renderNotificationMessage(
notMigratedMessage(props.noteLockMigrationStatus),
_('Retry'),
onMigrateLockedNotes,
_('Skip'),
() => void confirmAndSkipNoteLockKeyMigration(props.dispatch),
);
} else if (props.noteLockMigrationStatus) {
msg = renderNotificationMessage(
withSkippedCount(_('The migration of your locked notes has completed.'), props.noteLockMigrationStatus),
_('Dismiss'),
onDismissNoteLockMigrationStatus,
);
} else if (props.showNoteLockKeyConflictMessage) {
msg = renderNotificationMessage(
_('Synchronisation is stopped because the sync target uses a different note lock key to the one on your device.'),
_('Migrate your locked notes'),
onMigrateLockedNotes,
);
} else if (props.shouldUpgradeSyncTarget) {
msg = renderNotificationMessage(
_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.'),
_('Restart and upgrade'),
onRestartAndUpgrade,
);
} else if (props.hasDisabledEncryptionItems) {
msg = renderNotificationMessage(
_('Some items cannot be decrypted.'),
_('View them now'),
onViewStatusScreen,
);
} else if (props.showNeedUpgradingMasterKeyMessage) {
msg = renderNotificationMessage(
_('One of your master keys use an obsolete encryption method.'),
_('View them now'),
onViewEncryptionConfigScreen,
);
} else if (props.showShouldReencryptMessage) {
msg = renderNotificationMessage(
_('The default encryption method has been changed, you should re-encrypt your data.'),
_('More info'),
onViewEncryptionConfigScreen,
);
} else if (showShareInvitationNotification()) {
const invitation = props.shareInvitations.find(inv => inv.status === 0);
const sharer = invitation.share.user;
msg = renderNotificationMessage(
_('%s (%s) would like to share a notebook with you.', sharer.full_name, sharer.email),
_('Accept'),
() => onInvitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, true),
_('Reject'),
() => onInvitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, false),
);
} else if (props.hasDisabledSyncItems) {
msg = renderNotificationMessage(
_('Some items cannot be synchronised.'),
_('View them now'),
onViewStatusScreen,
);
} else if (props.showMissingMasterKeyMessage) {
msg = renderNotificationMessage(
_('One or more master keys need a password.'),
_('Set the password'),
onViewEncryptionConfigScreen,
);
} else if (props.mustUpgradeAppMessage) {
if (!props.syncTargetAppMinVersion) {
msg = renderNotificationMessage(props.mustUpgradeAppMessage);
} else if (appMinVersionState.didReleaseLoadFail) {
msg = renderNotificationMessage(
_(
'In order to synchronise, Please upgrade your application to version %s. Joplin could not check update information.',
props.syncTargetAppMinVersion,
),
);
} else if (appMinVersionState.isRelease === false && shim.isLinux()) {
const callForAction = _('Download it from GitHub Releases');
msg = renderNotificationMessage(
_(
'In order to synchronise, Please upgrade your application to version %s: %s or update it using your package manager',
props.syncTargetAppMinVersion,
callForAction,
),
callForAction,
() => shim.openUrl('https://github.com/laurent22/joplin/releases'),
);
} else if (appMinVersionState.isRelease !== null) {
const isTargetPreRelease = appMinVersionState.isRelease === false;
const callForAction = isTargetPreRelease ? _('Download it from GitHub Releases') : _('Check for updates');
msg = renderNotificationMessage(
_(
'In order to synchronise, Please upgrade your application to version %s: %s',
props.syncTargetAppMinVersion,
callForAction,
),
callForAction,
isTargetPreRelease ? () => shim.openUrl('https://github.com/laurent22/joplin/releases') : onCheckForUpdates,
);
} else {
msg = renderNotificationMessage(props.mustUpgradeAppMessage);
}
} else if (props.showUnknownKeyFormatMessage) {
msg = renderNotificationMessage(
_('One or more encryption keys are stored in an unknown format.'),
_('Manage'),
onViewEncryptionConfigScreen,
);
} else if (props.shouldSwitchToAppleSiliconVersion) {
msg = renderNotificationMessage(
_('You are running the Intel version of Joplin on an Apple Silicon processor. Download the Apple Silicon one for better performance.'),
_('Download it now'),
onDownloadAppleSiliconVersion,
);
} else if (props.showInvalidJoplinCloudCredential) {
msg = renderNotificationMessage(
_('Your Joplin Cloud credentials are invalid, please login.'),
_('Login to Joplin Cloud.'),
onViewJoplinCloudLoginScreen,
_('Disable synchronisation'),
onDisableSync,
);
}
const visible = !!msg;
const lastVisibleRef = useRef(false);
useEffect(() => {
if (visible !== lastVisibleRef.current) {
lastVisibleRef.current = visible;
if (visible) {
propsRef.current.onShow();
} else {
propsRef.current.onHide();
}
}
}, [visible]);
if (!visible) return null;
return (
<div
style={{ height: props.height }}
className='warning-banner -header'
>
<span
className='content'
role='alert'
// role='alert' has an implicit aria-live='assertive', which tells screen readers that changes
// to the warning's content should be announced as soon as possible. However, since it's generally
// okay for announcements related to these notifications to be delayed, use aria-live='polite'.
aria-live='polite'
>{msg}</span>
</div>
);
};
const mapStateToProps = (state: AppState) => {
const syncInfo = localSyncInfoFromState(state);
const showNeedUpgradingEnabledMasterKeyMessage = !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys.filter((k) => !!k.enabled)).length;
return {
hasDisabledSyncItems: state.hasDisabledSyncItems,
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
showUnknownKeyFormatMessage: showUnknownKeyFormatBanner(syncInfo),
showNoteLockKeyConflictMessage: noteLockKeyConflictFromState(state),
noteLockMigrationStatus: state.noteLockMigrationStatus,
showNeedUpgradingMasterKeyMessage: showNeedUpgradingEnabledMasterKeyMessage,
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
hasMissingSyncCredentials: shouldShowMissingPasswordWarning(state.settings['sync.target'], state.settings),
shareInvitations: state.shareService.shareInvitations,
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
isSafeMode: state.settings.isSafeMode,
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
syncTargetAppMinVersion: state.mustUpgradeAppMinVersion,
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && shim.isMac() && process.arch !== 'arm64',
};
};
export default connect(mapStateToProps)(WarningBanner);
type CallForActionHandler = ()=> void;
interface NotificationMessageProps {
message: string;
callForAction: string|null;
callForActionHandler: CallForActionHandler|null;
callForAction2: string|null;
callForActionHandler2: CallForActionHandler|null;
}
const NotificationMessage: React.FC<NotificationMessageProps> = ({ message, callForAction, callForActionHandler, callForAction2, callForActionHandler2 }) => {
if (!callForAction) return <span>{message}</span>;
const cfa = (
<a href="#" className='warning-banner-link -underline' onClick={() => callForActionHandler()}>
{callForAction}
</a>
);
const cfa2 = !callForAction2 ? null : (
<a href="#" className='warning-banner-link -underline' onClick={() => callForActionHandler2()}>
{callForAction2}
</a>
);
if (!callForAction2 && message.includes(callForAction)) {
const actionIndex = message.indexOf(callForAction);
return (
<span>
{message.substring(0, actionIndex)}
{cfa}
{message.substring(actionIndex + callForAction.length)}
</span>
);
}
return (
<span>
{message}{callForAction ? ' ' : ''}
{cfa}{callForAction2 ? ' / ' : ''}{cfa2}
</span>
);
};
interface AppMinVersionIsReleaseProps {
appMinVersion: string;
mustUpgradeAppMessage: string;
}
const useAppMinVersionState = (props: AppMinVersionIsReleaseProps) => {
const version = props.appMinVersion;
const [state, setState] = useState({
isRelease: null,
didReleaseLoadFail: false,
});
useAsyncEffect(async (event) => {
setState({ isRelease: null, didReleaseLoadFail: false });
if (!props.mustUpgradeAppMessage || !version) return;
try {
const isRelease = await isReleaseVersion(version);
if (event.cancelled) return;
setState({
isRelease,
didReleaseLoadFail: isRelease === null,
});
} catch (error) {
logger.error(error);
if (event.cancelled) return;
setState(state => ({
...state,
didReleaseLoadFail: true,
}));
}
}, [version, props.mustUpgradeAppMessage]);
return state;
};