From 3719a73c817a8324e33be71f18888ec66e76766b Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 18 May 2022 17:13:14 +0800 Subject: [PATCH] Add announcement modal and open it after prefs sync'd. - Don't want to show it in Incognito. - Only show it in when entered from homepage, or in the Help page. - Record the hash of the viewed announcement and update the wallet with it. --- ui/component/app/index.js | 11 +++- ui/component/app/view.jsx | 12 ++++- ui/constants/modal_types.js | 1 + ui/modal/modalAnnouncements/index.js | 20 +++++++ ui/modal/modalAnnouncements/view.jsx | 68 ++++++++++++++++++++++++ ui/modal/modalRouter/view.jsx | 1 + ui/redux/actions/settings.js | 46 ++++++++++------ ui/scss/component/_markdown-preview.scss | 23 ++++++++ ui/util/string.js | 4 ++ 9 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 ui/modal/modalAnnouncements/index.js create mode 100644 ui/modal/modalAnnouncements/view.jsx diff --git a/ui/component/app/index.js b/ui/component/app/index.js index 179a3aec0..03fe67c8e 100644 --- a/ui/component/app/index.js +++ b/ui/component/app/index.js @@ -1,6 +1,11 @@ import { hot } from 'react-hot-loader/root'; import { connect } from 'react-redux'; -import { selectGetSyncErrorMessage, selectSyncFatalError, selectSyncIsLocked } from 'redux/selectors/sync'; +import { + selectGetSyncErrorMessage, + selectPrefsReady, + selectSyncFatalError, + selectSyncIsLocked, +} from 'redux/selectors/sync'; import { doUserSetReferrer } from 'redux/actions/user'; import { selectOdyseeMembershipIsPremiumPlus, @@ -15,7 +20,7 @@ import { selectMyChannelClaimIds } from 'redux/selectors/claims'; import { selectLanguage, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; import { selectModal, selectActiveChannelClaim, selectIsReloadRequired } from 'redux/selectors/app'; import { selectUploadCount } from 'redux/selectors/publish'; -import { doSetLanguage } from 'redux/actions/settings'; +import { doOpenAnnouncements, doSetLanguage } from 'redux/actions/settings'; import { doSyncLoop } from 'redux/actions/sync'; import { doSignIn, doSetIncognito } from 'redux/actions/app'; import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/comments'; @@ -28,6 +33,7 @@ const select = (state) => ({ language: selectLanguage(state), languages: selectLoadedLanguages(state), isReloadRequired: selectIsReloadRequired(state), + prefsReady: selectPrefsReady(state), syncError: selectGetSyncErrorMessage(state), syncIsLocked: selectSyncIsLocked(state), uploadCount: selectUploadCount(state), @@ -51,6 +57,7 @@ const perform = { setIncognito: doSetIncognito, fetchModBlockedList: doFetchModBlockedList, fetchModAmIList: doFetchCommentModAmIList, + doOpenAnnouncements, }; export default hot(connect(select, perform)(App)); diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index e1b40ee5f..941140da5 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -74,6 +74,7 @@ type Props = { balance: ?number, syncIsLocked: boolean, syncError: ?string, + prefsReady: boolean, rewards: Array, setReferrer: (string, boolean) => void, isAuthenticated: boolean, @@ -87,6 +88,7 @@ type Props = { fetchModBlockedList: () => void, fetchModAmIList: () => void, homepageFetched: boolean, + doOpenAnnouncements: () => void, }; function App(props: Props) { @@ -103,6 +105,7 @@ function App(props: Props) { history, syncError, syncIsLocked, + prefsReady, language, languages, setLanguage, @@ -119,6 +122,7 @@ function App(props: Props) { hasPremiumPlus, fetchModAmIList, homepageFetched, + doOpenAnnouncements, } = props; const isMobile = useIsMobile(); @@ -438,7 +442,7 @@ function App(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps, (one time after locale is fetched) }, [locale]); - React.useEffect(() => { + useEffect(() => { if (locale) { const countryCode = locale.country; const langs = getLanguagesForCountry(countryCode) || []; @@ -471,6 +475,12 @@ function App(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncError, pathname, isAuthenticated]); + useEffect(() => { + if (prefsReady) { + doOpenAnnouncements(); + } + }, [prefsReady]); + // Keep this at the end to ensure initial setup effects are run first useEffect(() => { if (!hasSignedIn && hasVerifiedEmail) { diff --git a/ui/constants/modal_types.js b/ui/constants/modal_types.js index 0f8bc6463..baa1adba8 100644 --- a/ui/constants/modal_types.js +++ b/ui/constants/modal_types.js @@ -51,3 +51,4 @@ export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT'; export const CONFIRM_ODYSEE_MEMBERSHIP = 'CONFIRM_ODYSEE_MEMBERSHIP'; export const MEMBERSHIP_SPLASH = 'MEMBERSHIP_SPLASH'; export const HIDE_RECOMMENDATION = 'HIDE_RECOMMENDATION'; +export const ANNOUNCEMENTS = 'ANNOUNCEMENTS'; diff --git a/ui/modal/modalAnnouncements/index.js b/ui/modal/modalAnnouncements/index.js new file mode 100644 index 000000000..38a404915 --- /dev/null +++ b/ui/modal/modalAnnouncements/index.js @@ -0,0 +1,20 @@ +import ModalAnnouncements from './view'; +import { connect } from 'react-redux'; +import { doHideModal } from 'redux/actions/app'; +import { doSetLastViewedAnnouncement } from 'redux/actions/content'; +import { selectLastViewedAnnouncement } from 'redux/selectors/content'; +import { selectHomepageAnnouncement } from 'redux/selectors/settings'; +import { selectUserVerifiedEmail } from 'redux/selectors/user'; + +const select = (state) => ({ + authenticated: selectUserVerifiedEmail(state), + announcement: selectHomepageAnnouncement(state), + lastViewedHash: selectLastViewedAnnouncement(state), +}); + +const perform = { + doHideModal, + doSetLastViewedAnnouncement, +}; + +export default connect(select, perform)(ModalAnnouncements); diff --git a/ui/modal/modalAnnouncements/view.jsx b/ui/modal/modalAnnouncements/view.jsx new file mode 100644 index 000000000..e9b657fac --- /dev/null +++ b/ui/modal/modalAnnouncements/view.jsx @@ -0,0 +1,68 @@ +// @flow +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import Card from 'component/common/card'; +import MarkdownPreview from 'component/common/markdown-preview'; +import * as PAGES from 'constants/pages'; +import { Modal } from 'modal/modal'; +import { getSimpleStrHash } from 'util/string'; + +type Props = { + isAutoInvoked?: boolean, + // --- redux --- + authenticated?: boolean, + announcement: string, + lastViewedHash: ?string, + doHideModal: () => void, + doSetLastViewedAnnouncement: (hash: string) => void, +}; + +export default function ModalAnnouncements(props: Props) { + const { + authenticated, + announcement, + lastViewedHash, + isAutoInvoked, + doHideModal, + doSetLastViewedAnnouncement, + } = props; + + const { + location: { pathname }, + } = useHistory(); + + const [show, setShow] = React.useState(false); + + React.useEffect(() => { + if (!authenticated || (pathname !== '/' && pathname !== `/$/${PAGES.HELP}`) || announcement === '') { + doHideModal(); + return; + } + + const hash = getSimpleStrHash(announcement); + + if (lastViewedHash === hash) { + if (isAutoInvoked) { + doHideModal(); + } else { + setShow(true); + } + } else { + setShow(true); + doSetLastViewedAnnouncement(hash); + } + }, []); + + if (!show) { + return null; + } + + return ( + + } + /> + + ); +} diff --git a/ui/modal/modalRouter/view.jsx b/ui/modal/modalRouter/view.jsx index 46c85a640..407101658 100644 --- a/ui/modal/modalRouter/view.jsx +++ b/ui/modal/modalRouter/view.jsx @@ -7,6 +7,7 @@ import { lazyImport } from 'util/lazyImport'; // prettier-ignore const MAP = Object.freeze({ + [MODALS.ANNOUNCEMENTS]: lazyImport(() => import('modal/modalAnnouncements' /* webpackChunkName: "modalAnnouncements" */)), [MODALS.AFFIRM_PURCHASE]: lazyImport(() => import('modal/modalAffirmPurchase' /* webpackChunkName: "modalAffirmPurchase" */)), [MODALS.AUTO_GENERATE_THUMBNAIL]: lazyImport(() => import('modal/modalAutoGenerateThumbnail' /* webpackChunkName: "modalAutoGenerateThumbnail" */)), [MODALS.AUTO_UPDATE_DOWNLOADED]: lazyImport(() => import('modal/modalAutoUpdateDownloaded' /* webpackChunkName: "modalAutoUpdateDownloaded" */)), diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js index eddee70b5..38729dee2 100644 --- a/ui/redux/actions/settings.js +++ b/ui/redux/actions/settings.js @@ -3,6 +3,7 @@ import { doWalletReconnect } from 'redux/actions/wallet'; import * as SETTINGS from 'constants/settings'; import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as ACTIONS from 'constants/action_types'; +import * as MODALS from 'constants/modal_types'; import * as SHARED_PREFERENCES from 'constants/shared_preferences'; import { doToast } from 'redux/actions/notifications'; import analytics from 'analytics'; @@ -10,7 +11,7 @@ import SUPPORTED_LANGUAGES from 'constants/supported_languages'; import { launcher } from 'util/autoLaunch'; import { selectClientSetting } from 'redux/selectors/settings'; import { doSyncLoop, doSyncUnsubscribe, doSetSyncLock } from 'redux/actions/sync'; -import { doAlertWaitingForSync, doGetAndPopulatePreferences } from 'redux/actions/app'; +import { doAlertWaitingForSync, doGetAndPopulatePreferences, doOpenModal } from 'redux/actions/app'; import { selectPrefsReady } from 'redux/selectors/sync'; import { Lbryio } from 'lbryinc'; import { getDefaultLanguage } from 'util/default-languages'; @@ -317,23 +318,38 @@ function populateCategoryTitles(categories) { } } +function loadBuiltInHomepageData(dispatch) { + const homepages = require('homepages'); + if (homepages) { + const v2 = {}; + const homepageKeys = Object.keys(homepages); + homepageKeys.forEach((hp) => { + v2[hp] = homepages[hp]; + }); + + window.homepages = v2; + populateCategoryTitles(window.homepages?.en?.categories); + dispatch({ type: ACTIONS.FETCH_HOMEPAGES_DONE }); + } +} + +export function doOpenAnnouncements() { + return (dispatch) => { + // There is a weird useEffect in modalRouter that closes all modals on + // initial mount. Not sure what scenario that covers, so just delay a bit + // until it is mounted. + setTimeout(() => { + dispatch(doOpenModal(MODALS.ANNOUNCEMENTS, { isAutoInvoked: true })); + }, 1000); + }; +} + export function doFetchHomepages() { return (dispatch) => { - // -- Use this env flag to use local homepage data. Otherwise, it will grab from `/$/api/content/v*/get`. + // -- Use this env flag to use local homepage data and meme (faster). + // -- Otherwise, it will grab from `/$/api/content/v*/get`. // @if USE_LOCAL_HOMEPAGE_DATA='true' - const homepages = require('homepages'); - if (homepages) { - const v2 = {}; - const homepageKeys = Object.keys(homepages); - homepageKeys.forEach((hp) => { - v2[hp] = homepages[hp]; - }); - - window.homepages = v2; - populateCategoryTitles(window.homepages?.en?.categories); - dispatch({ type: ACTIONS.FETCH_HOMEPAGES_DONE }); - return; - } + loadBuiltInHomepageData(dispatch); // @endif fetch('https://odysee.com/$/api/content/v2/get') diff --git a/ui/scss/component/_markdown-preview.scss b/ui/scss/component/_markdown-preview.scss index d29080d6b..d81d8f69d 100644 --- a/ui/scss/component/_markdown-preview.scss +++ b/ui/scss/component/_markdown-preview.scss @@ -303,6 +303,29 @@ } } +// TODO: Move .announcement to `modalAnnouncements/style.scss` when structure is ready. +.announcement { + @media (min-width: $breakpoint-small) { + max-height: 90vh; + } +} + +.markdown-preview--announcement { + @media (min-width: $breakpoint-small) { + max-height: 80vh; + } + + overflow: hidden; + overflow-y: auto; + + img { + max-width: 40%; + display: block; + margin-left: auto; + margin-right: auto; + } +} + .editor-preview { background-color: rgba(var(--color-header-background-base), 1); } diff --git a/ui/util/string.js b/ui/util/string.js index f71a36677..b736e9362 100644 --- a/ui/util/string.js +++ b/ui/util/string.js @@ -26,3 +26,7 @@ export function toCompactNotation(number: string | number, lang: ?string, minThr export function stripLeadingAtSign(str: ?string) { return str && str.charAt(0) === '@' ? str.slice(1) : str; } + +export function getSimpleStrHash(s: string) { + return String(s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)); +}