mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add announcements and news in the homepage community tab (#4273)
* Also display "urgent" announcements at the top of the home page (which can be dismissed).
This commit is contained in:
91
newIDE/app/src/AnnouncementsFeed/AnnouncementsFeedContext.js
Normal file
91
newIDE/app/src/AnnouncementsFeed/AnnouncementsFeedContext.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type Announcement,
|
||||
listAllAnnouncements,
|
||||
} from '../Utils/GDevelopServices/Announcement';
|
||||
|
||||
type AnnouncementsFeedState = {|
|
||||
announcements: ?(Announcement[]),
|
||||
error: ?Error,
|
||||
fetchAnnouncements: () => void,
|
||||
|};
|
||||
|
||||
export const AnnouncementsFeedContext = React.createContext<AnnouncementsFeedState>(
|
||||
{
|
||||
announcements: null,
|
||||
error: null,
|
||||
fetchAnnouncements: () => {},
|
||||
}
|
||||
);
|
||||
|
||||
type AnnouncementsFeedStateProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export const AnnouncementsFeedStateProvider = ({
|
||||
children,
|
||||
}: AnnouncementsFeedStateProviderProps) => {
|
||||
const [announcements, setAnnouncements] = React.useState<?(Announcement[])>(
|
||||
null
|
||||
);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const isLoading = React.useRef<boolean>(false);
|
||||
|
||||
const fetchAnnouncements = React.useCallback(
|
||||
() => {
|
||||
if (isLoading.current) return;
|
||||
|
||||
(async () => {
|
||||
setError(null);
|
||||
isLoading.current = true;
|
||||
|
||||
try {
|
||||
const announcements = await listAllAnnouncements();
|
||||
|
||||
setAnnouncements(announcements);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Unable to load the announcements from the api:`,
|
||||
error
|
||||
);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
isLoading.current = false;
|
||||
})();
|
||||
},
|
||||
[isLoading]
|
||||
);
|
||||
|
||||
// Preload the assets and filters when the app loads.
|
||||
React.useEffect(
|
||||
() => {
|
||||
// Don't attempt to load again assets and filters if they
|
||||
// were loaded already.
|
||||
if (announcements || isLoading.current) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.info('Pre-fetching announcements from the api...');
|
||||
fetchAnnouncements();
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
[fetchAnnouncements, announcements, isLoading]
|
||||
);
|
||||
|
||||
const announcementsFeedState = React.useMemo(
|
||||
() => ({
|
||||
announcements,
|
||||
error,
|
||||
fetchAnnouncements,
|
||||
}),
|
||||
[announcements, error, fetchAnnouncements]
|
||||
);
|
||||
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider value={announcementsFeedState}>
|
||||
{children}
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
100
newIDE/app/src/AnnouncementsFeed/index.js
Normal file
100
newIDE/app/src/AnnouncementsFeed/index.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import * as React from 'react';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import { selectMessageByLocale } from '../Utils/i18n/MessageByLocale';
|
||||
import Window from '../Utils/Window';
|
||||
import { AnnouncementsFeedContext } from './AnnouncementsFeedContext';
|
||||
import Text from '../UI/Text';
|
||||
import { Line } from '../UI/Grid';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
|
||||
type AnnouncementsFeedProps = {|
|
||||
level?: 'urgent' | 'normal',
|
||||
canClose?: boolean,
|
||||
addMargins?: boolean,
|
||||
|};
|
||||
|
||||
export const AnnouncementsFeed = ({
|
||||
level,
|
||||
canClose,
|
||||
addMargins,
|
||||
}: AnnouncementsFeedProps) => {
|
||||
const { announcements, error, fetchAnnouncements } = React.useContext(
|
||||
AnnouncementsFeedContext
|
||||
);
|
||||
const { values, showAnnouncement } = React.useContext(PreferencesContext);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PlaceholderError onRetry={fetchAnnouncements}>
|
||||
<Trans>
|
||||
Can't load the announcements. Verify your internet connection or try
|
||||
again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
);
|
||||
} else if (!announcements) {
|
||||
return <PlaceholderLoader />;
|
||||
}
|
||||
|
||||
const properLevelAnnouncements = level
|
||||
? announcements.filter(announcement => announcement.level === level)
|
||||
: announcements;
|
||||
|
||||
const displayedAnnouncements = canClose
|
||||
? properLevelAnnouncements.filter(announcement => {
|
||||
return !values.hiddenAnnouncements[announcement.id];
|
||||
})
|
||||
: properLevelAnnouncements;
|
||||
|
||||
if (!displayedAnnouncements.length) return null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Line noMargin={!addMargins}>
|
||||
<ColumnStackLayout noMargin={!addMargins} expand>
|
||||
{displayedAnnouncements.map(announcement => (
|
||||
<AlertMessage
|
||||
kind={announcement.type === 'warning' ? 'warning' : 'info'}
|
||||
renderRightButton={() => (
|
||||
<RaisedButton
|
||||
label={selectMessageByLocale(
|
||||
i18n,
|
||||
announcement.buttonLabelByLocale
|
||||
)}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(announcement.buttonUrl)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
onHide={
|
||||
canClose
|
||||
? () => {
|
||||
showAnnouncement(announcement.id, false);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Text size="block-title">
|
||||
<Trans>
|
||||
{selectMessageByLocale(i18n, announcement.titleByLocale)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
{selectMessageByLocale(i18n, announcement.messageByLocale)}
|
||||
</Text>
|
||||
</AlertMessage>
|
||||
))}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
@@ -14,6 +14,7 @@ import TikTok from '../../../UI/CustomSvgIcons/TikTok';
|
||||
import SectionContainer, { SectionRow } from './SectionContainer';
|
||||
import { ListItem } from '../../../UI/List';
|
||||
import List from '@material-ui/core/List';
|
||||
import { AnnouncementsFeed } from '../../../AnnouncementsFeed';
|
||||
|
||||
const styles = {
|
||||
list: {
|
||||
@@ -65,7 +66,11 @@ const CommunitySection = () => {
|
||||
return (
|
||||
<SectionContainer title={<Trans>Community</Trans>}>
|
||||
<SectionRow>
|
||||
<ColumnStackLayout alignItems="start" noMargin expand>
|
||||
<ColumnStackLayout noMargin expand>
|
||||
<Text size="title">
|
||||
<Trans>News and announcements</Trans>
|
||||
</Text>
|
||||
<AnnouncementsFeed canClose={false} />
|
||||
<Text size="title">
|
||||
<Trans>Join the conversation</Trans>
|
||||
</Text>
|
||||
|
@@ -123,7 +123,10 @@ export const HomePageMenu = ({
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
style={windowWidth === 'large' ? styles.desktopMenu : styles.mobileMenu}
|
||||
style={{
|
||||
...(windowWidth === 'large' ? styles.desktopMenu : styles.mobileMenu),
|
||||
borderRight: `1px solid ${GDevelopTheme.home.separator.color}`,
|
||||
}}
|
||||
square
|
||||
>
|
||||
<Column expand>
|
||||
|
@@ -4,7 +4,6 @@ import { Column, Line } from '../../../UI/Grid';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import Text from '../../../UI/Text';
|
||||
import GDevelopThemeContext from '../../../UI/Theme/ThemeContext';
|
||||
import ArrowLeft from '../../../UI/CustomSvgIcons/ArrowLeft';
|
||||
import TextButton from '../../../UI/TextButton';
|
||||
import { Trans } from '@lingui/macro';
|
||||
@@ -57,7 +56,7 @@ const SectionContainer = ({
|
||||
renderFooter,
|
||||
}: Props) => {
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const GDevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<Column useFullHeight noMargin expand>
|
||||
<Paper
|
||||
@@ -65,7 +64,6 @@ const SectionContainer = ({
|
||||
style={{
|
||||
...styles.scrollContainer,
|
||||
display: flexBody ? 'flex' : 'block',
|
||||
borderLeft: `1px solid ${GDevelopTheme.home.separator.color}`,
|
||||
...(windowWidth === 'small'
|
||||
? styles.mobileScrollContainer
|
||||
: styles.desktopScrollContainer),
|
||||
@@ -100,12 +98,9 @@ const SectionContainer = ({
|
||||
{renderFooter && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
style={{
|
||||
borderLeft: `1px solid ${GDevelopTheme.home.separator.color}`,
|
||||
...(windowWidth === 'small'
|
||||
? styles.mobileFooter
|
||||
: styles.desktopFooter),
|
||||
}}
|
||||
style={
|
||||
windowWidth === 'small' ? styles.mobileFooter : styles.desktopFooter
|
||||
}
|
||||
square
|
||||
>
|
||||
{renderFooter()}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { Line, Column } from '../../../UI/Grid';
|
||||
import { type RenderEditorContainerPropsWithRef } from '../BaseEditor';
|
||||
import {
|
||||
@@ -20,6 +21,8 @@ import { HomePageMenu, type HomeTab } from './HomePageMenu';
|
||||
import PreferencesContext from '../../Preferences/PreferencesContext';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
import { type ExampleShortHeader } from '../../../Utils/GDevelopServices/Example';
|
||||
import { AnnouncementsFeed } from '../../../AnnouncementsFeed';
|
||||
import { AnnouncementsFeedContext } from '../../../AnnouncementsFeed/AnnouncementsFeedContext';
|
||||
|
||||
type Props = {|
|
||||
project: ?gdProject,
|
||||
@@ -84,6 +87,7 @@ export const HomePage = React.memo<Props>(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const { fetchTutorials } = React.useContext(TutorialContext);
|
||||
const { announcements } = React.useContext(AnnouncementsFeedContext);
|
||||
const { fetchShowcasedGamesAndFilters } = React.useContext(
|
||||
GamesShowcaseContext
|
||||
);
|
||||
@@ -167,49 +171,56 @@ export const HomePage = React.memo<Props>(
|
||||
onOpenPreferences={onOpenPreferences}
|
||||
onOpenAbout={onOpenAbout}
|
||||
/>
|
||||
{activeTab === 'get-started' && (
|
||||
<GetStartedSection
|
||||
onTabChange={setActiveTab}
|
||||
onCreateProject={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onOpenOnboardingDialog={onOpenOnboardingDialog}
|
||||
showGetStartedSection={showGetStartedSection}
|
||||
setShowGetStartedSection={setShowGetStartedSection}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'build' && (
|
||||
<BuildSection
|
||||
ref={buildSectionRef}
|
||||
project={project}
|
||||
canOpen={canOpen}
|
||||
onChooseProject={onChooseProject}
|
||||
onOpenProjectPreCreationDialog={
|
||||
onOpenProjectPreCreationDialog
|
||||
}
|
||||
onShowAllExamples={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onSelectExample={exampleShortHeader =>
|
||||
onCreateProject(exampleShortHeader)
|
||||
}
|
||||
onOpenRecentFile={onOpenRecentFile}
|
||||
onChangeSubscription={onChangeSubscription}
|
||||
storageProviders={storageProviders}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'learn' && (
|
||||
<LearnSection
|
||||
onOpenOnboardingDialog={onOpenOnboardingDialog}
|
||||
onCreateProject={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onTabChange={setActiveTab}
|
||||
onOpenHelpFinder={onOpenHelpFinder}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'play' && <PlaySection />}
|
||||
{activeTab === 'community' && <CommunitySection />}
|
||||
<Column noMargin expand>
|
||||
{activeTab !== 'community' && !!announcements && (
|
||||
<Paper elevation={0} style={{}} square>
|
||||
<AnnouncementsFeed level="urgent" canClose addMargins />
|
||||
</Paper>
|
||||
)}
|
||||
{activeTab === 'get-started' && (
|
||||
<GetStartedSection
|
||||
onTabChange={setActiveTab}
|
||||
onCreateProject={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onOpenOnboardingDialog={onOpenOnboardingDialog}
|
||||
showGetStartedSection={showGetStartedSection}
|
||||
setShowGetStartedSection={setShowGetStartedSection}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'build' && (
|
||||
<BuildSection
|
||||
ref={buildSectionRef}
|
||||
project={project}
|
||||
canOpen={canOpen}
|
||||
onChooseProject={onChooseProject}
|
||||
onOpenProjectPreCreationDialog={
|
||||
onOpenProjectPreCreationDialog
|
||||
}
|
||||
onShowAllExamples={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onSelectExample={exampleShortHeader =>
|
||||
onCreateProject(exampleShortHeader)
|
||||
}
|
||||
onOpenRecentFile={onOpenRecentFile}
|
||||
onChangeSubscription={onChangeSubscription}
|
||||
storageProviders={storageProviders}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'learn' && (
|
||||
<LearnSection
|
||||
onOpenOnboardingDialog={onOpenOnboardingDialog}
|
||||
onCreateProject={() =>
|
||||
onCreateProject(/*exampleShortHeader=*/ null)
|
||||
}
|
||||
onTabChange={setActiveTab}
|
||||
onOpenHelpFinder={onOpenHelpFinder}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'play' && <PlaySection />}
|
||||
{activeTab === 'community' && <CommunitySection />}
|
||||
</Column>
|
||||
</Line>
|
||||
</Column>
|
||||
</>
|
||||
|
@@ -182,6 +182,7 @@ export type PreferencesValues = {|
|
||||
codeEditorThemeName: string,
|
||||
hiddenAlertMessages: { [AlertMessageIdentifier]: boolean },
|
||||
hiddenTutorialHints: { [string]: boolean },
|
||||
hiddenAnnouncements: { [string]: boolean },
|
||||
autoDisplayChangelog: boolean,
|
||||
lastLaunchedVersion: ?string,
|
||||
eventsSheetShowObjectThumbnails: boolean,
|
||||
@@ -222,6 +223,8 @@ export type Preferences = {|
|
||||
showAllAlertMessages: () => void,
|
||||
showTutorialHint: (identifier: string, show: boolean) => void,
|
||||
showAllTutorialHints: () => void,
|
||||
showAnnouncement: (identifier: string, show: boolean) => void,
|
||||
showAllAnnouncements: () => void,
|
||||
verifyIfIsNewVersion: () => boolean,
|
||||
setEventsSheetShowObjectThumbnails: (enabled: boolean) => void,
|
||||
setAutosaveOnPreview: (enabled: boolean) => void,
|
||||
@@ -280,6 +283,7 @@ export const initialPreferences = {
|
||||
codeEditorThemeName: 'vs-dark',
|
||||
hiddenAlertMessages: {},
|
||||
hiddenTutorialHints: {},
|
||||
hiddenAnnouncements: {},
|
||||
autoDisplayChangelog: true,
|
||||
lastLaunchedVersion: undefined,
|
||||
eventsSheetShowObjectThumbnails: true,
|
||||
@@ -314,6 +318,8 @@ export const initialPreferences = {
|
||||
showAllAlertMessages: () => {},
|
||||
showTutorialHint: (identifier: string, show: boolean) => {},
|
||||
showAllTutorialHints: () => {},
|
||||
showAnnouncement: (identifier: string, show: boolean) => {},
|
||||
showAllAnnouncements: () => {},
|
||||
verifyIfIsNewVersion: () => false,
|
||||
setEventsSheetShowObjectThumbnails: () => {},
|
||||
setAutosaveOnPreview: () => {},
|
||||
|
@@ -40,6 +40,7 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
setAutoDownloadUpdates,
|
||||
showAllAlertMessages,
|
||||
showAllTutorialHints,
|
||||
showAllAnnouncements,
|
||||
setAutoDisplayChangelog,
|
||||
setEventsSheetShowObjectThumbnails,
|
||||
setAutosaveOnPreview,
|
||||
@@ -290,6 +291,13 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
disabled={!Object.keys(values.hiddenTutorialHints).length}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<RaisedButton
|
||||
label={<Trans>Reset hidden announcements</Trans>}
|
||||
onClick={() => showAllAnnouncements()}
|
||||
disabled={!Object.keys(values.hiddenAnnouncements).length}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
<Text size="block-title">
|
||||
<Trans>Advanced</Trans>
|
||||
|
@@ -99,6 +99,8 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
showAllAlertMessages: this._showAllAlertMessages.bind(this),
|
||||
showTutorialHint: this._showTutorialHint.bind(this),
|
||||
showAllTutorialHints: this._showAllTutorialHints.bind(this),
|
||||
showAnnouncement: this._showAnnouncement.bind(this),
|
||||
showAllAnnouncements: this._showAllAnnouncements.bind(this),
|
||||
verifyIfIsNewVersion: this._verifyIfIsNewVersion.bind(this),
|
||||
setEventsSheetShowObjectThumbnails: this._setEventsSheetShowObjectThumbnails.bind(
|
||||
this
|
||||
@@ -447,6 +449,33 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
_showAnnouncement(identifier: string, show: boolean) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
hiddenAnnouncements: {
|
||||
...state.values.hiddenAnnouncements,
|
||||
[identifier]: !show,
|
||||
},
|
||||
},
|
||||
}),
|
||||
() => this._persistValuesToLocalStorage(this.state)
|
||||
);
|
||||
}
|
||||
|
||||
_showAllAnnouncements() {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
hiddenAnnouncements: {},
|
||||
},
|
||||
}),
|
||||
() => this._persistValuesToLocalStorage(this.state)
|
||||
);
|
||||
}
|
||||
|
||||
_persistValuesToLocalStorage(preferences: Preferences) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
|
@@ -33,6 +33,7 @@ import { ExtensionStoreStateProvider } from '../AssetStore/ExtensionStore/Extens
|
||||
import { GamesShowcaseStateProvider } from '../GamesShowcase/GamesShowcaseContext';
|
||||
import { TutorialStateProvider } from '../Tutorial/TutorialContext';
|
||||
import ConfirmProvider from '../UI/Confirm/ConfirmProvider';
|
||||
import { AnnouncementsFeedStateProvider } from '../AnnouncementsFeed/AnnouncementsFeedContext';
|
||||
|
||||
// Add the rtl plugin to the JSS instance to support RTL languages in material-ui components.
|
||||
const jss = create({
|
||||
@@ -105,7 +106,9 @@ export default class Providers extends React.Component<Props, {||}> {
|
||||
<ExtensionStoreStateProvider>
|
||||
<GamesShowcaseStateProvider>
|
||||
<TutorialStateProvider>
|
||||
{children({ i18n })}
|
||||
<AnnouncementsFeedStateProvider>
|
||||
{children({ i18n })}
|
||||
</AnnouncementsFeedStateProvider>
|
||||
</TutorialStateProvider>
|
||||
</GamesShowcaseStateProvider>
|
||||
</ExtensionStoreStateProvider>
|
||||
|
@@ -22,7 +22,7 @@ const styles = {
|
||||
type Props = {|
|
||||
kind: 'info' | 'warning' | 'error',
|
||||
children: React.Node,
|
||||
onHide?: () => void,
|
||||
onHide?: ?() => void,
|
||||
renderLeftIcon?: () => React.Node,
|
||||
renderRightButton?: () => React.Node,
|
||||
|};
|
||||
|
21
newIDE/app/src/Utils/GDevelopServices/Announcement.js
Normal file
21
newIDE/app/src/Utils/GDevelopServices/Announcement.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import axios from 'axios';
|
||||
import { GDevelopReleaseApi } from './ApiConfigs';
|
||||
import { type MessageByLocale } from '../i18n/MessageByLocale';
|
||||
|
||||
export type Announcement = {
|
||||
id: string,
|
||||
titleByLocale: MessageByLocale,
|
||||
messageByLocale: MessageByLocale,
|
||||
type: 'info' | 'warning',
|
||||
level: 'normal' | 'urgent',
|
||||
buttonUrl: string,
|
||||
buttonLabelByLocale: MessageByLocale,
|
||||
};
|
||||
|
||||
export const listAllAnnouncements = async (): Promise<Array<Announcement>> => {
|
||||
const response = await axios.get(
|
||||
`${GDevelopReleaseApi.baseUrl}/announcement`
|
||||
);
|
||||
return response.data;
|
||||
};
|
27
newIDE/app/src/Utils/i18n/MessageByLocale.js
Normal file
27
newIDE/app/src/Utils/i18n/MessageByLocale.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
export type MessageByLocale = { [string]: string };
|
||||
|
||||
export const selectMessageByLocale = (
|
||||
i18n: I18nType,
|
||||
messageByLocale: MessageByLocale
|
||||
): string => {
|
||||
if (!messageByLocale) return '';
|
||||
if (typeof messageByLocale === 'string') return messageByLocale;
|
||||
if (typeof messageByLocale !== 'object') return '';
|
||||
|
||||
const language = i18n.language;
|
||||
|
||||
if (messageByLocale[language]) return messageByLocale[language];
|
||||
|
||||
const languageFirstCode = language.split('-')[0];
|
||||
if (messageByLocale[languageFirstCode])
|
||||
return messageByLocale[languageFirstCode];
|
||||
|
||||
if (messageByLocale['en']) return messageByLocale['en'];
|
||||
|
||||
const firstLanguage = Object.keys(messageByLocale)[0];
|
||||
if (messageByLocale[firstLanguage]) return messageByLocale[firstLanguage];
|
||||
|
||||
return '';
|
||||
};
|
110
newIDE/app/src/Utils/i18n/MessageByLocale.spec.js
Normal file
110
newIDE/app/src/Utils/i18n/MessageByLocale.spec.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { selectMessageByLocale } from './MessageByLocale';
|
||||
|
||||
// $FlowExpectedError
|
||||
const makeFakeI18n = (fakeI18n): I18nType => fakeI18n;
|
||||
|
||||
describe('MessageByLocale', () => {
|
||||
describe('selectMessageByLocale', () => {
|
||||
test('select the proper message according to the language', () => {
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'en' }), {
|
||||
en: 'Test',
|
||||
})
|
||||
).toBe('Test');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'en' }), {
|
||||
en: 'Test',
|
||||
fr: 'Test2',
|
||||
})
|
||||
).toBe('Test');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), {
|
||||
en: 'Test',
|
||||
fr: 'Test2',
|
||||
})
|
||||
).toBe('Test2');
|
||||
});
|
||||
test('fallback to the same language even if not fully qualifying for the region', () => {
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'pt-br' }), {
|
||||
'pt-pt': 'Message 1',
|
||||
'pt-br': 'Message 2',
|
||||
})
|
||||
).toBe('Message 2');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'pt-OTHER' }), {
|
||||
'pt-pt': 'Message 1',
|
||||
pt: 'Message 2',
|
||||
})
|
||||
).toBe('Message 2');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'pt-pt' }), {
|
||||
'pt-pt': 'Message 1',
|
||||
pt: 'Message 1',
|
||||
})
|
||||
).toBe('Message 1');
|
||||
});
|
||||
test('fallback to english or the only language available', () => {
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'en' }), {
|
||||
fr: 'Only this is available.',
|
||||
})
|
||||
).toBe('Only this is available.');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'pt-BR' }), {
|
||||
en: 'Test',
|
||||
fr: 'Test2',
|
||||
})
|
||||
).toBe('Test');
|
||||
expect(
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'pt-OTHER' }), {
|
||||
'pt-pt': 'Message 1',
|
||||
'pt-br': 'Message 2',
|
||||
en: 'Message 3',
|
||||
})
|
||||
).toBe('Message 3');
|
||||
expect(selectMessageByLocale(makeFakeI18n({ language: 'fr' }), {})).toBe(
|
||||
''
|
||||
);
|
||||
});
|
||||
test('handles type errors gracefully', () => {
|
||||
expect(
|
||||
// $FlowExpectedError
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), 'Test')
|
||||
).toBe('Test');
|
||||
|
||||
// $FlowExpectedError
|
||||
expect(selectMessageByLocale(makeFakeI18n({ language: 'fr' }), 0)).toBe(
|
||||
''
|
||||
);
|
||||
|
||||
// $FlowExpectedError
|
||||
expect(selectMessageByLocale(makeFakeI18n({ language: 'fr' }), 123)).toBe(
|
||||
''
|
||||
);
|
||||
|
||||
// $FlowExpectedError
|
||||
expect(selectMessageByLocale(makeFakeI18n({ language: 'fr' }), [])).toBe(
|
||||
''
|
||||
);
|
||||
expect(
|
||||
// $FlowExpectedError
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), null)
|
||||
).toBe('');
|
||||
expect(
|
||||
// $FlowExpectedError
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), undefined)
|
||||
).toBe('');
|
||||
expect(
|
||||
// $FlowExpectedError
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), false)
|
||||
).toBe('');
|
||||
expect(
|
||||
// $FlowExpectedError
|
||||
selectMessageByLocale(makeFakeI18n({ language: 'fr' }), true)
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
@@ -27,6 +27,7 @@ import {
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import { formatISO, subDays } from 'date-fns';
|
||||
import { type Comment } from '../../Utils/GDevelopServices/Play';
|
||||
import { type Announcement } from '../../Utils/GDevelopServices/Announcement';
|
||||
|
||||
export const indieFirebaseUser: FirebaseUser = {
|
||||
uid: 'indie-user',
|
||||
@@ -1279,3 +1280,58 @@ export const commentProcessed: Comment = {
|
||||
updatedAt: 1515084393000,
|
||||
processedAt: 1515084393000,
|
||||
};
|
||||
|
||||
export const fakeAnnouncements: Announcement[] = [
|
||||
{
|
||||
id: '123',
|
||||
type: 'info',
|
||||
level: 'normal',
|
||||
titleByLocale: {
|
||||
en: 'Some title',
|
||||
},
|
||||
messageByLocale: {
|
||||
en: 'Something to announce which is really really cool',
|
||||
},
|
||||
buttonLabelByLocale: { en: 'View' },
|
||||
buttonUrl: 'https://gdevelop.io',
|
||||
},
|
||||
{
|
||||
id: '124',
|
||||
type: 'info',
|
||||
level: 'urgent',
|
||||
titleByLocale: {
|
||||
en: 'Some title',
|
||||
},
|
||||
messageByLocale: {
|
||||
en: 'Something nothing important but urgent to announce.',
|
||||
},
|
||||
buttonLabelByLocale: { en: 'View' },
|
||||
buttonUrl: 'https://gdevelop.io',
|
||||
},
|
||||
{
|
||||
id: '125',
|
||||
type: 'warning',
|
||||
level: 'urgent',
|
||||
titleByLocale: {
|
||||
en: 'Some title',
|
||||
},
|
||||
messageByLocale: {
|
||||
en: 'Something important and urgent to announce.',
|
||||
},
|
||||
buttonLabelByLocale: { en: 'View' },
|
||||
buttonUrl: 'https://gdevelop.io',
|
||||
},
|
||||
{
|
||||
id: '126',
|
||||
type: 'warning',
|
||||
level: 'normal',
|
||||
titleByLocale: {
|
||||
en: 'Some title',
|
||||
},
|
||||
messageByLocale: {
|
||||
en: 'Something important but not urgent to announce.',
|
||||
},
|
||||
buttonLabelByLocale: { en: 'View' },
|
||||
buttonUrl: 'https://gdevelop.io',
|
||||
},
|
||||
];
|
||||
|
@@ -0,0 +1,86 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
|
||||
import { AnnouncementsFeed } from '../../../AnnouncementsFeed';
|
||||
import { AnnouncementsFeedContext } from '../../../AnnouncementsFeed/AnnouncementsFeedContext';
|
||||
import { fakeAnnouncements } from '../../../fixtures/GDevelopServicesTestData';
|
||||
|
||||
export default {
|
||||
title: 'AnnouncementsFeed',
|
||||
component: AnnouncementsFeed,
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
export const ErrorLoadingAnnouncements = () => {
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider
|
||||
value={{
|
||||
announcements: null,
|
||||
error: new Error('Fake error'),
|
||||
fetchAnnouncements: action('fetchAnnouncements'),
|
||||
}}
|
||||
>
|
||||
<AnnouncementsFeed />
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingAnnouncements = () => {
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider
|
||||
value={{
|
||||
announcements: null,
|
||||
error: null,
|
||||
fetchAnnouncements: action('fetchAnnouncements'),
|
||||
}}
|
||||
>
|
||||
<AnnouncementsFeed />
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider
|
||||
value={{
|
||||
announcements: fakeAnnouncements,
|
||||
error: null,
|
||||
fetchAnnouncements: action('fetchAnnouncements'),
|
||||
}}
|
||||
>
|
||||
<AnnouncementsFeed />
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultWithMargins = () => {
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider
|
||||
value={{
|
||||
announcements: fakeAnnouncements,
|
||||
error: null,
|
||||
fetchAnnouncements: action('fetchAnnouncements'),
|
||||
}}
|
||||
>
|
||||
<AnnouncementsFeed addMargins />
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const OnlyUrgent = () => {
|
||||
return (
|
||||
<AnnouncementsFeedContext.Provider
|
||||
value={{
|
||||
announcements: fakeAnnouncements,
|
||||
error: null,
|
||||
fetchAnnouncements: action('fetchAnnouncements'),
|
||||
}}
|
||||
>
|
||||
<AnnouncementsFeed level="urgent" />
|
||||
</AnnouncementsFeedContext.Provider>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user