一些检查失败了
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
433 行
15 KiB
TypeScript
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;
|
|
};
|