Settings Page: add warning for unsaved settings (#430)

* Settings Page: add warning for unsaved settings

## Issue
When entering Settings Page, sync-loop is disable until user exist Settings Page.  If browser is closed, changes will be lost.

## Change
Add the usual browser-level modal popup.

Note that all modern browsers have stopped supporting customized messages, but I still left the message there for clarity.  Tried to use our own toast for it, but the handler locks all GUI until it is serviced.

* app: remove unused props

* app: use lighter selectors

When all we need is to know if something exists or their count, use the ID version instead of the url/claim version to avoid the heavy transformation.
This commit is contained in:
infinite-persistence 2021-12-06 06:38:26 -08:00 committed by GitHub
parent fb7c5d0fff
commit 08ebedb4cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 28 additions and 18 deletions

View file

@ -2242,5 +2242,6 @@
"Switch to Credits": "Switch to Credits", "Switch to Credits": "Switch to Credits",
"Cookies": "Cookies", "Cookies": "Cookies",
"Did someone invite you to use Odysee? Tell us who and you both get a reward!": "Did someone invite you to use Odysee? Tell us who and you both get a reward!", "Did someone invite you to use Odysee? Tell us who and you both get a reward!": "Did someone invite you to use Odysee? Tell us who and you both get a reward!",
"There are unsaved settings. Exit the Settings Page to finalize them.": "There are unsaved settings. Exit the Settings Page to finalize them.",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -1,19 +1,18 @@
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors/sync'; import { selectGetSyncErrorMessage, selectSyncFatalError, selectSyncIsLocked } from 'redux/selectors/sync';
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user'; import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectUnclaimedRewards } from 'redux/selectors/rewards'; import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims'; import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims';
import { selectMyChannelUrls } from 'redux/selectors/claims'; import { selectMyChannelClaimIds } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectClientSetting, selectLanguage, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; import { selectLanguage, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
import { import {
selectIsUpgradeAvailable, selectIsUpgradeAvailable,
selectAutoUpdateDownloaded, selectAutoUpdateDownloaded,
selectModal, selectModal,
selectActiveChannelClaim, selectActiveChannelId,
selectIsReloadRequired, selectIsReloadRequired,
} from 'redux/selectors/app'; } from 'redux/selectors/app';
import { selectUploadCount } from 'redux/selectors/publish'; import { selectUploadCount } from 'redux/selectors/publish';
@ -28,19 +27,19 @@ const select = (state) => ({
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
theme: selectThemePath(state), theme: selectThemePath(state),
language: selectLanguage(state), language: selectLanguage(state),
syncEnabled: selectClientSetting(state, SETTINGS.ENABLE_SYNC),
languages: selectLoadedLanguages(state), languages: selectLoadedLanguages(state),
autoUpdateDownloaded: selectAutoUpdateDownloaded(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
isUpgradeAvailable: selectIsUpgradeAvailable(state), isUpgradeAvailable: selectIsUpgradeAvailable(state),
isReloadRequired: selectIsReloadRequired(state), isReloadRequired: selectIsReloadRequired(state),
syncError: selectGetSyncErrorMessage(state), syncError: selectGetSyncErrorMessage(state),
syncIsLocked: selectSyncIsLocked(state),
uploadCount: selectUploadCount(state), uploadCount: selectUploadCount(state),
rewards: selectUnclaimedRewards(state), rewards: selectUnclaimedRewards(state),
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
currentModal: selectModal(state), currentModal: selectModal(state),
syncFatalError: selectSyncFatalError(state), syncFatalError: selectSyncFatalError(state),
activeChannelClaim: selectActiveChannelClaim(state), activeChannelId: selectActiveChannelId(state),
myChannelUrls: selectMyChannelUrls(state), myChannelClaimIds: selectMyChannelClaimIds(state),
subscriptions: selectSubscriptions(state), subscriptions: selectSubscriptions(state),
}); });

View file

@ -62,24 +62,22 @@ type Props = {
fetchCollectionListMine: () => void, fetchCollectionListMine: () => void,
signIn: () => void, signIn: () => void,
requestDownloadUpgrade: () => void, requestDownloadUpgrade: () => void,
onSignedIn: () => void,
setLanguage: (string) => void, setLanguage: (string) => void,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
isReloadRequired: boolean, isReloadRequired: boolean,
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
uploadCount: number, uploadCount: number,
balance: ?number, balance: ?number,
syncIsLocked: boolean,
syncError: ?string, syncError: ?string,
syncEnabled: boolean,
rewards: Array<Reward>, rewards: Array<Reward>,
setReferrer: (string, boolean) => void, setReferrer: (string, boolean) => void,
isAuthenticated: boolean, isAuthenticated: boolean,
socketConnect: () => void,
syncLoop: (?boolean) => void, syncLoop: (?boolean) => void,
currentModal: any, currentModal: any,
syncFatalError: boolean, syncFatalError: boolean,
activeChannelClaim: ?ChannelClaim, activeChannelId: ?string,
myChannelUrls: ?Array<string>, myChannelClaimIds: ?Array<string>,
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
setActiveChannelIfNotSet: () => void, setActiveChannelIfNotSet: () => void,
setIncognito: (boolean) => void, setIncognito: (boolean) => void,
@ -103,6 +101,7 @@ function App(props: Props) {
uploadCount, uploadCount,
history, history,
syncError, syncError,
syncIsLocked,
language, language,
languages, languages,
setLanguage, setLanguage,
@ -112,8 +111,8 @@ function App(props: Props) {
syncLoop, syncLoop,
currentModal, currentModal,
syncFatalError, syncFatalError,
myChannelUrls, myChannelClaimIds,
activeChannelClaim, activeChannelId,
setActiveChannelIfNotSet, setActiveChannelIfNotSet,
setIncognito, setIncognito,
fetchModBlockedList, fetchModBlockedList,
@ -150,10 +149,10 @@ function App(props: Props) {
const shouldHideNag = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`); const shouldHideNag = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`);
const userId = user && user.id; const userId = user && user.id;
const useCustomScrollbar = !IS_MAC; const useCustomScrollbar = !IS_MAC;
const hasMyChannels = myChannelUrls && myChannelUrls.length > 0; const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
const hasNoChannels = myChannelUrls && myChannelUrls.length === 0; const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language]; const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
const hasActiveChannelClaim = activeChannelClaim !== undefined; const hasActiveChannelClaim = activeChannelId !== undefined;
const isPersonalized = !IS_WEB || hasVerifiedEmail; const isPersonalized = !IS_WEB || hasVerifiedEmail;
const renderFiledrop = !IS_WEB || isAuthenticated; const renderFiledrop = !IS_WEB || isAuthenticated;
const isOnline = navigator.onLine; const isOnline = navigator.onLine;
@ -219,6 +218,17 @@ function App(props: Props) {
} }
}, [userId]); }, [userId]);
useEffect(() => {
if (syncIsLocked) {
const handleBeforeUnload = (event) => {
event.preventDefault();
event.returnValue = __('There are unsaved settings. Exit the Settings Page to finalize them.');
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}
}, [syncIsLocked]);
useEffect(() => { useEffect(() => {
if (!uploadCount) return; if (!uploadCount) return;
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {