Announcements framework (#1494)

This commit is contained in:
infinite-persistence 2022-05-20 00:36:29 +08:00
commit 581ae13c3f
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
24 changed files with 299 additions and 27 deletions

View file

@ -15,6 +15,7 @@ declare type ContentState = {
// It can/should be '?Array<string>` instead -- set it to null, then clients // It can/should be '?Array<string>` instead -- set it to null, then clients
// can cast it to a boolean. That, or rename the variable to `shuffle` if you // can cast it to a boolean. That, or rename the variable to `shuffle` if you
// don't care about the URLs. // don't care about the URLs.
lastViewedAnnouncement: ?string, // undefined = not seen in wallet.
}; };
declare type WatchHistory = { declare type WatchHistory = {

View file

@ -2259,6 +2259,8 @@
"Still Valid Until": "Still Valid Until", "Still Valid Until": "Still Valid Until",
"Active channel": "Active channel", "Active channel": "Active channel",
"Select your default active channel": "Select your default active channel", "Select your default active channel": "Select your default active channel",
"What's New": "What's New",
"See what are the latest features and changes in Odysee.": "See what are the latest features and changes in Odysee.",
"This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.": "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.", "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.": "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.",
"Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.", "Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.",
"There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.": "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.", "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.": "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.",

View file

@ -1,7 +1,13 @@
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, selectSyncIsLocked } from 'redux/selectors/sync'; import {
selectGetSyncErrorMessage,
selectPrefsReady,
selectSyncFatalError,
selectSyncIsLocked,
} from 'redux/selectors/sync';
import { doUserSetReferrer } from 'redux/actions/user'; import { doUserSetReferrer } from 'redux/actions/user';
import { doSetLastViewedAnnouncement } from 'redux/actions/content';
import { import {
selectOdyseeMembershipIsPremiumPlus, selectOdyseeMembershipIsPremiumPlus,
selectUser, selectUser,
@ -15,7 +21,7 @@ import { selectMyChannelClaimIds } from 'redux/selectors/claims';
import { selectLanguage, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; import { selectLanguage, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
import { selectModal, selectActiveChannelClaim, selectIsReloadRequired } from 'redux/selectors/app'; import { selectModal, selectActiveChannelClaim, selectIsReloadRequired } from 'redux/selectors/app';
import { selectUploadCount } from 'redux/selectors/publish'; 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 { doSyncLoop } from 'redux/actions/sync';
import { doSignIn, doSetIncognito } from 'redux/actions/app'; import { doSignIn, doSetIncognito } from 'redux/actions/app';
import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/comments'; import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/comments';
@ -28,6 +34,7 @@ const select = (state) => ({
language: selectLanguage(state), language: selectLanguage(state),
languages: selectLoadedLanguages(state), languages: selectLoadedLanguages(state),
isReloadRequired: selectIsReloadRequired(state), isReloadRequired: selectIsReloadRequired(state),
prefsReady: selectPrefsReady(state),
syncError: selectGetSyncErrorMessage(state), syncError: selectGetSyncErrorMessage(state),
syncIsLocked: selectSyncIsLocked(state), syncIsLocked: selectSyncIsLocked(state),
uploadCount: selectUploadCount(state), uploadCount: selectUploadCount(state),
@ -51,6 +58,8 @@ const perform = {
setIncognito: doSetIncognito, setIncognito: doSetIncognito,
fetchModBlockedList: doFetchModBlockedList, fetchModBlockedList: doFetchModBlockedList,
fetchModAmIList: doFetchCommentModAmIList, fetchModAmIList: doFetchCommentModAmIList,
doOpenAnnouncements,
doSetLastViewedAnnouncement,
}; };
export default hot(connect(select, perform)(App)); export default hot(connect(select, perform)(App));

View file

@ -74,6 +74,7 @@ type Props = {
balance: ?number, balance: ?number,
syncIsLocked: boolean, syncIsLocked: boolean,
syncError: ?string, syncError: ?string,
prefsReady: boolean,
rewards: Array<Reward>, rewards: Array<Reward>,
setReferrer: (string, boolean) => void, setReferrer: (string, boolean) => void,
isAuthenticated: boolean, isAuthenticated: boolean,
@ -87,6 +88,8 @@ type Props = {
fetchModBlockedList: () => void, fetchModBlockedList: () => void,
fetchModAmIList: () => void, fetchModAmIList: () => void,
homepageFetched: boolean, homepageFetched: boolean,
doOpenAnnouncements: () => void,
doSetLastViewedAnnouncement: (hash: string) => void,
}; };
function App(props: Props) { function App(props: Props) {
@ -103,6 +106,7 @@ function App(props: Props) {
history, history,
syncError, syncError,
syncIsLocked, syncIsLocked,
prefsReady,
language, language,
languages, languages,
setLanguage, setLanguage,
@ -119,6 +123,8 @@ function App(props: Props) {
hasPremiumPlus, hasPremiumPlus,
fetchModAmIList, fetchModAmIList,
homepageFetched, homepageFetched,
doOpenAnnouncements,
doSetLastViewedAnnouncement,
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -438,7 +444,7 @@ function App(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps, (one time after locale is fetched) // eslint-disable-next-line react-hooks/exhaustive-deps, (one time after locale is fetched)
}, [locale]); }, [locale]);
React.useEffect(() => { useEffect(() => {
if (locale) { if (locale) {
const countryCode = locale.country; const countryCode = locale.country;
const langs = getLanguagesForCountry(countryCode) || []; const langs = getLanguagesForCountry(countryCode) || [];
@ -471,6 +477,19 @@ function App(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncError, pathname, isAuthenticated]); }, [syncError, pathname, isAuthenticated]);
useEffect(() => {
if (prefsReady) {
doOpenAnnouncements();
}
}, [prefsReady]);
useEffect(() => {
window.clearLastViewedAnnouncement = () => {
console.log('Clearing history. Please wait ...');
doSetLastViewedAnnouncement('');
};
}, []);
// Keep this at the end to ensure initial setup effects are run first // Keep this at the end to ensure initial setup effects are run first
useEffect(() => { useEffect(() => {
if (!hasSignedIn && hasVerifiedEmail) { if (!hasSignedIn && hasVerifiedEmail) {

View file

@ -143,6 +143,7 @@ export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED'; export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI'; export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL'; export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
export const SET_LAST_VIEWED_ANNOUNCEMENT = 'SET_LAST_VIEWED_ANNOUNCEMENT';
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED'; export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED'; export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST'; export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';

View file

@ -51,3 +51,4 @@ export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT';
export const CONFIRM_ODYSEE_MEMBERSHIP = 'CONFIRM_ODYSEE_MEMBERSHIP'; export const CONFIRM_ODYSEE_MEMBERSHIP = 'CONFIRM_ODYSEE_MEMBERSHIP';
export const MEMBERSHIP_SPLASH = 'MEMBERSHIP_SPLASH'; export const MEMBERSHIP_SPLASH = 'MEMBERSHIP_SPLASH';
export const HIDE_RECOMMENDATION = 'HIDE_RECOMMENDATION'; export const HIDE_RECOMMENDATION = 'HIDE_RECOMMENDATION';
export const ANNOUNCEMENTS = 'ANNOUNCEMENTS';

View file

@ -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);

View file

@ -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 (
<Modal type="card" isOpen onAborted={doHideModal}>
<Card
className="announcement"
actions={<MarkdownPreview className="markdown-preview--announcement" content={announcement} simpleLinks />}
/>
</Modal>
);
}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectModal } from 'redux/selectors/app'; import { selectModal } from 'redux/selectors/app';
import { doOpenModal, doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { selectError } from 'redux/selectors/notifications'; // RENAME THIS 'selectNotificationError' import { selectError } from 'redux/selectors/notifications'; // RENAME THIS 'selectNotificationError'
import ModalRouter from './view'; import ModalRouter from './view';
@ -9,9 +9,8 @@ const select = (state, props) => ({
error: selectError(state), error: selectError(state),
}); });
const perform = (dispatch) => ({ const perform = {
openModal: (props) => dispatch(doOpenModal(props)), hideModal: doHideModal,
hideModal: (props) => dispatch(doHideModal(props)), };
});
export default connect(select, perform)(ModalRouter); export default connect(select, perform)(ModalRouter);

View file

@ -7,6 +7,7 @@ import { lazyImport } from 'util/lazyImport';
// prettier-ignore // prettier-ignore
const MAP = Object.freeze({ const MAP = Object.freeze({
[MODALS.ANNOUNCEMENTS]: lazyImport(() => import('modal/modalAnnouncements' /* webpackChunkName: "modalAnnouncements" */)),
[MODALS.AFFIRM_PURCHASE]: lazyImport(() => import('modal/modalAffirmPurchase' /* webpackChunkName: "modalAffirmPurchase" */)), [MODALS.AFFIRM_PURCHASE]: lazyImport(() => import('modal/modalAffirmPurchase' /* webpackChunkName: "modalAffirmPurchase" */)),
[MODALS.AUTO_GENERATE_THUMBNAIL]: lazyImport(() => import('modal/modalAutoGenerateThumbnail' /* webpackChunkName: "modalAutoGenerateThumbnail" */)), [MODALS.AUTO_GENERATE_THUMBNAIL]: lazyImport(() => import('modal/modalAutoGenerateThumbnail' /* webpackChunkName: "modalAutoGenerateThumbnail" */)),
[MODALS.AUTO_UPDATE_DOWNLOADED]: lazyImport(() => import('modal/modalAutoUpdateDownloaded' /* webpackChunkName: "modalAutoUpdateDownloaded" */)), [MODALS.AUTO_UPDATE_DOWNLOADED]: lazyImport(() => import('modal/modalAutoUpdateDownloaded' /* webpackChunkName: "modalAutoUpdateDownloaded" */)),

View file

@ -1,3 +1,14 @@
import HelpPage from './view'; import HelpPage from './view';
import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import { selectHomepageAnnouncement } from 'redux/selectors/settings';
export default HelpPage; const select = (state) => ({
announcement: selectHomepageAnnouncement(state),
});
const perform = {
doOpenModal,
};
export default connect(select, perform)(HelpPage);

View file

@ -7,9 +7,35 @@ import Page from 'component/page';
import Card from 'component/common/card'; import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
export default function HelpPage() { import * as MODALS from 'constants/modal_types';
type Props = {
announcement: string,
doOpenModal: (string, ?{}) => void,
};
export default function HelpPage(props: Props) {
const { announcement, doOpenModal } = props;
return ( return (
<Page className="card-stack"> <Page className="card-stack">
{announcement && (
<Card
title={__("What's New")}
subtitle={__('See what are the latest features and changes in Odysee.')}
actions={
<div className="section__actions">
<Button
label={__("What's New")}
icon={ICONS.FEEDBACK}
button="secondary"
onClick={() => doOpenModal(MODALS.ANNOUNCEMENTS)}
/>
</div>
}
/>
)}
<Card <Card
title={__('Visit the %SITE_NAME% Help Hub', { SITE_NAME })} title={__('Visit the %SITE_NAME% Help Hub', { SITE_NAME })}
subtitle={__('Our support posts answer many common questions.')} subtitle={__('Our support posts answer many common questions.')}

View file

@ -374,3 +374,12 @@ export function doToggleShuffleList(currentUri: string, collectionId: string, sh
} }
}; };
} }
export function doSetLastViewedAnnouncement(hash: string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.SET_LAST_VIEWED_ANNOUNCEMENT,
data: hash,
});
};
}

View file

@ -3,6 +3,7 @@ import { doWalletReconnect } from 'redux/actions/wallet';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as DAEMON_SETTINGS from 'constants/daemon_settings';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types';
import * as SHARED_PREFERENCES from 'constants/shared_preferences'; import * as SHARED_PREFERENCES from 'constants/shared_preferences';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import analytics from 'analytics'; import analytics from 'analytics';
@ -10,7 +11,7 @@ import SUPPORTED_LANGUAGES from 'constants/supported_languages';
import { launcher } from 'util/autoLaunch'; import { launcher } from 'util/autoLaunch';
import { selectClientSetting } from 'redux/selectors/settings'; import { selectClientSetting } from 'redux/selectors/settings';
import { doSyncLoop, doSyncUnsubscribe, doSetSyncLock } from 'redux/actions/sync'; 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 { selectPrefsReady } from 'redux/selectors/sync';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { getDefaultLanguage } from 'util/default-languages'; 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() { export function doFetchHomepages() {
return (dispatch) => { 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' // @if USE_LOCAL_HOMEPAGE_DATA='true'
const homepages = require('homepages'); loadBuiltInHomepageData(dispatch);
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;
}
// @endif // @endif
fetch('https://odysee.com/$/api/content/v2/get') fetch('https://odysee.com/$/api/content/v2/get')

View file

@ -421,6 +421,7 @@ type SharedData = {
editedCollections: CollectionGroup, editedCollections: CollectionGroup,
builtinCollections: CollectionGroup, builtinCollections: CollectionGroup,
savedCollections: Array<string>, savedCollections: Array<string>,
lastViewedAnnouncement: string,
}, },
}; };
@ -439,6 +440,7 @@ function extractUserState(rawObj: SharedData) {
editedCollections, editedCollections,
builtinCollections, builtinCollections,
savedCollections, savedCollections,
lastViewedAnnouncement,
} = rawObj.value; } = rawObj.value;
return { return {
@ -454,6 +456,7 @@ function extractUserState(rawObj: SharedData) {
...(editedCollections ? { editedCollections } : {}), ...(editedCollections ? { editedCollections } : {}),
...(builtinCollections ? { builtinCollections } : {}), ...(builtinCollections ? { builtinCollections } : {}),
...(savedCollections ? { savedCollections } : {}), ...(savedCollections ? { savedCollections } : {}),
...(lastViewedAnnouncement ? { lastViewedAnnouncement } : {}),
}; };
} }
@ -475,6 +478,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
editedCollections, editedCollections,
builtinCollections, builtinCollections,
savedCollections, savedCollections,
lastViewedAnnouncement,
} = extractUserState(sharedSettings); } = extractUserState(sharedSettings);
dispatch({ dispatch({
type: ACTIONS.USER_STATE_POPULATE, type: ACTIONS.USER_STATE_POPULATE,
@ -491,6 +495,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
editedCollections, editedCollections,
builtinCollections, builtinCollections,
savedCollections, savedCollections,
lastViewedAnnouncement,
}, },
}); });
}; };

View file

@ -13,6 +13,7 @@ const defaultState: ContentState = {
recommendationUrls: {}, recommendationUrls: {},
recommendationClicks: {}, recommendationClicks: {},
loopList: undefined, loopList: undefined,
lastViewedAnnouncement: '',
}; };
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) => reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
@ -118,6 +119,8 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] }); reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] });
reducers[ACTIONS.SET_LAST_VIEWED_ANNOUNCEMENT] = (state, action) => ({ ...state, lastViewedAnnouncement: action.data });
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => { // reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
// return { // return {
// ...state, // ...state,
@ -125,6 +128,11 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [
// }; // };
// }; // };
reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => {
const { lastViewedAnnouncement } = action.data;
return { ...state, lastViewedAnnouncement };
};
export default function reducer(state: ContentState = defaultState, action: any) { export default function reducer(state: ContentState = defaultState, action: any) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);

View file

@ -26,6 +26,7 @@ export const selectPlayingUri = (state: State) => selectState(state).playingUri;
export const selectPrimaryUri = (state: State) => selectState(state).primaryUri; export const selectPrimaryUri = (state: State) => selectState(state).primaryUri;
export const selectListLoop = (state: State) => selectState(state).loopList; export const selectListLoop = (state: State) => selectState(state).loopList;
export const selectListShuffle = (state: State) => selectState(state).shuffleList; export const selectListShuffle = (state: State) => selectState(state).shuffleList;
export const selectLastViewedAnnouncement = (state: State) => selectState(state).lastViewedAnnouncement;
export const makeSelectIsPlaying = (uri: string) => export const makeSelectIsPlaying = (uri: string) =>
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri); createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);

View file

@ -82,6 +82,18 @@ export const selectHomepageMeme = (state) => {
return homepages ? homepages['en'].meme || {} : {}; return homepages ? homepages['en'].meme || {} : {};
}; };
export const selectHomepageAnnouncement = (state) => {
const homepageCode = selectHomepageCode(state);
const homepages = window.homepages;
if (homepages) {
const news = homepages[homepageCode].announcement;
if (news) {
return news;
}
}
return homepages ? homepages['en'].announcement || '' : '';
};
export const selectInRegionByCode = (state, code) => { export const selectInRegionByCode = (state, code) => {
const hp = selectClientSetting(state, SETTINGS.HOMEPAGE); const hp = selectClientSetting(state, SETTINGS.HOMEPAGE);
const lang = selectLanguage(state); const lang = selectLanguage(state);

View file

@ -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 { .editor-preview {
background-color: rgba(var(--color-header-background-base), 1); background-color: rgba(var(--color-header-background-base), 1);
} }

View file

@ -45,7 +45,7 @@ function enableBatching(reducer) {
}; };
} }
const contentFilter = createFilter('content', ['positions', 'history']); const contentFilter = createFilter('content', ['positions', 'history', 'lastViewedAnnouncement']);
const fileInfoFilter = createFilter('fileInfo', [ const fileInfoFilter = createFilter('fileInfo', [
'fileListPublishedSort', 'fileListPublishedSort',
'fileListDownloadedSort', 'fileListDownloadedSort',
@ -141,6 +141,7 @@ const triggerSharedStateActions = [
ACTIONS.COLLECTION_DELETE, ACTIONS.COLLECTION_DELETE,
ACTIONS.COLLECTION_NEW, ACTIONS.COLLECTION_NEW,
ACTIONS.COLLECTION_PENDING, ACTIONS.COLLECTION_PENDING,
ACTIONS.SET_LAST_VIEWED_ANNOUNCEMENT,
// MAYBE COLLECTOIN SAVE // MAYBE COLLECTOIN SAVE
// ACTIONS.SET_WELCOME_VERSION, // ACTIONS.SET_WELCOME_VERSION,
// ACTIONS.SET_ALLOW_ANALYTICS, // ACTIONS.SET_ALLOW_ANALYTICS,
@ -183,6 +184,7 @@ const sharedStateFilters = {
editedCollections: { source: 'collections', property: 'edited' }, editedCollections: { source: 'collections', property: 'edited' },
// savedCollections: { source: 'collections', property: 'saved' }, // savedCollections: { source: 'collections', property: 'saved' },
unpublishedCollections: { source: 'collections', property: 'unpublished' }, unpublishedCollections: { source: 'collections', property: 'unpublished' },
lastViewedAnnouncement: { source: 'content', property: 'lastViewedAnnouncement' },
}; };
const sharedStateCb = ({ dispatch, getState, syncId }) => { const sharedStateCb = ({ dispatch, getState, syncId }) => {

View file

@ -23,3 +23,15 @@ export function getLocalStorageSummary() {
return 'inaccessible'; return 'inaccessible';
} }
} }
const localStorageAvailable = isLocalStorageAvailable();
export function getLocalStorageItem(key) {
return localStorageAvailable ? window.localStorage.getItem(key) : undefined;
}
export function setLocalStorageItem(key, value) {
if (localStorageAvailable) {
window.localStorage.setItem(key, value);
}
}

View file

@ -26,3 +26,7 @@ export function toCompactNotation(number: string | number, lang: ?string, minThr
export function stripLeadingAtSign(str: ?string) { export function stripLeadingAtSign(str: ?string) {
return str && str.charAt(0) === '@' ? str.slice(1) : str; 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));
}

View file

@ -1,9 +1,24 @@
const path = require('path');
const memo = {}; const memo = {};
const loadAnnouncements = (homepageKeys) => {
const fs = require('fs');
const announcements = {};
homepageKeys.forEach((key) => {
const file = path.join(__dirname, `../dist/announcement/${key.toLowerCase()}.md`);
const announcement = fs.readFileSync(file, 'utf8');
announcements[key] = announcement ? announcement.trim() : '';
});
return announcements;
};
// this didn't seem to help. // this didn't seem to help.
if (!memo.homepageData) { if (!memo.homepageData) {
try { try {
memo.homepageData = require('../../custom/homepages/v2'); memo.homepageData = require('../../custom/homepages/v2');
memo.announcements = loadAnnouncements(Object.keys(memo.homepageData));
} catch (err) { } catch (err) {
console.log('getHomepageJSON:', err); console.log('getHomepageJSON:', err);
} }
@ -30,7 +45,10 @@ const getHomepageJsonV2 = () => {
const v2 = {}; const v2 = {};
const homepageKeys = Object.keys(memo.homepageData); const homepageKeys = Object.keys(memo.homepageData);
homepageKeys.forEach((hp) => { homepageKeys.forEach((hp) => {
v2[hp] = memo.homepageData[hp]; v2[hp] = {
...memo.homepageData[hp],
announcement: memo.announcements[hp],
};
}); });
return v2; return v2;
}; };

View file

@ -68,6 +68,10 @@ const copyWebpackCommands = [
from: `${WEB_STATIC_ROOT}/pwa/`, from: `${WEB_STATIC_ROOT}/pwa/`,
to: `${DIST_ROOT}/public/pwa/`, to: `${DIST_ROOT}/public/pwa/`,
}, },
{
from: `${STATIC_ROOT}/../custom/homepages/v2/announcement`,
to: `${DIST_ROOT}/announcement`,
},
]; ];
const CUSTOM_OG_PATH = `${CUSTOM_ROOT}/v2-og.png`; const CUSTOM_OG_PATH = `${CUSTOM_ROOT}/v2-og.png`;