forked from vergnet/application-amicale
Allow mascot popup to be controlled directly with a pref key
This commit is contained in:
parent
6254ce1814
commit
5349e210cb
7 changed files with 92 additions and 148 deletions
|
@ -7,9 +7,9 @@ import * as Animatable from "react-native-animatable";
|
|||
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import SpeechArrow from "./SpeechArrow";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
|
||||
type Props = {
|
||||
visible: boolean,
|
||||
theme: CustomTheme,
|
||||
icon: string,
|
||||
title: string,
|
||||
|
@ -19,34 +19,34 @@ type Props = {
|
|||
message: string,
|
||||
icon: string | null,
|
||||
color: string | null,
|
||||
onPress: () => void,
|
||||
onPress?: () => void,
|
||||
},
|
||||
cancel: {
|
||||
message: string,
|
||||
icon: string | null,
|
||||
color: string | null,
|
||||
onPress: () => void,
|
||||
onPress?: () => void,
|
||||
}
|
||||
},
|
||||
emotion: number,
|
||||
visible?: boolean,
|
||||
prefKey?: string,
|
||||
}
|
||||
|
||||
type State = {
|
||||
shouldShowDialog: boolean;
|
||||
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
|
||||
dialogVisible: boolean,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component used to display a popup with the mascot.
|
||||
*/
|
||||
class MascotPopup extends React.Component<Props, State> {
|
||||
|
||||
mascotSize: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
|
||||
state = {
|
||||
shouldShowDialog: this.props.visible,
|
||||
};
|
||||
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
|
@ -54,18 +54,40 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
this.windowHeight = Dimensions.get('window').height;
|
||||
|
||||
this.mascotSize = Dimensions.get('window').height / 6;
|
||||
|
||||
if (this.props.visible != null) {
|
||||
this.state = {
|
||||
shouldRenderDialog: this.props.visible,
|
||||
dialogVisible: this.props.visible,
|
||||
};
|
||||
} else if (this.props.prefKey != null) {
|
||||
const visible = AsyncStorageManager.getBool(this.props.prefKey);
|
||||
this.state = {
|
||||
shouldRenderDialog: visible,
|
||||
dialogVisible: visible,
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
shouldRenderDialog: false,
|
||||
dialogVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onAnimationEnd = () => {
|
||||
this.setState({
|
||||
shouldShowDialog: this.props.visible,
|
||||
shouldRenderDialog: false,
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
||||
if (nextProps.visible) {
|
||||
this.state.shouldShowDialog = true;
|
||||
} else if (nextProps.visible !== this.props.visible) {
|
||||
this.state.shouldRenderDialog = true;
|
||||
this.state.dialogVisible = true;
|
||||
} else if (nextProps.visible !== this.props.visible
|
||||
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
|
||||
this.state.dialogVisible = false;
|
||||
setTimeout(this.onAnimationEnd, 300);
|
||||
}
|
||||
return true;
|
||||
|
@ -79,13 +101,13 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
onBackButtonPressAndroid = () => {
|
||||
if (this.state.shouldShowDialog) {
|
||||
if (this.state.dialogVisible) {
|
||||
const cancel = this.props.buttons.cancel;
|
||||
const action = this.props.buttons.action;
|
||||
if (cancel != null)
|
||||
cancel.onPress();
|
||||
this.onDismiss(cancel.onPress);
|
||||
else
|
||||
action.onPress();
|
||||
this.onDismiss(action.onPress);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -100,8 +122,8 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
marginRight: "10%",
|
||||
}}
|
||||
useNativeDriver={true}
|
||||
animation={this.props.visible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.props.visible ? 1000 : 300}
|
||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.state.dialogVisible ? 1000 : 300}
|
||||
>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.mascotSize / 3}}
|
||||
|
@ -149,8 +171,8 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
return (
|
||||
<Animatable.View
|
||||
useNativeDriver={true}
|
||||
animation={this.props.visible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.props.visible ? 1500 : 200}
|
||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.state.dialogVisible ? 1500 : 200}
|
||||
>
|
||||
<Mascot
|
||||
style={{width: this.mascotSize}}
|
||||
|
@ -181,7 +203,7 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
mode={"contained"}
|
||||
icon={action.icon}
|
||||
color={action.color}
|
||||
onPress={action.onPress}
|
||||
onPress={() => this.onDismiss(action.onPress)}
|
||||
>
|
||||
{action.message}
|
||||
</Button>
|
||||
|
@ -195,7 +217,7 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
mode={"contained"}
|
||||
icon={cancel.icon}
|
||||
color={cancel.color}
|
||||
onPress={cancel.onPress}
|
||||
onPress={() => this.onDismiss(cancel.onPress)}
|
||||
>
|
||||
{cancel.message}
|
||||
</Button>
|
||||
|
@ -206,7 +228,7 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
|
||||
getBackground() {
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={this.props.buttons.cancel.onPress}>
|
||||
<TouchableWithoutFeedback onPress={() => this.onDismiss(this.props.buttons.cancel.onPress)}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
position: "absolute",
|
||||
|
@ -215,16 +237,25 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
height: "100%",
|
||||
}}
|
||||
useNativeDriver={true}
|
||||
animation={this.props.visible ? "fadeIn" : "fadeOut"}
|
||||
duration={this.props.visible ? 300 : 300}
|
||||
animation={this.state.dialogVisible ? "fadeIn" : "fadeOut"}
|
||||
duration={this.state.dialogVisible ? 300 : 300}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
onDismiss = (callback?: ()=> void) => {
|
||||
if (this.props.prefKey != null) {
|
||||
AsyncStorageManager.set(this.props.prefKey, false);
|
||||
this.setState({dialogVisible: false});
|
||||
}
|
||||
if (callback != null)
|
||||
callback();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.shouldShowDialog) {
|
||||
if (this.state.shouldRenderDialog) {
|
||||
return (
|
||||
<Portal>
|
||||
{this.getBackground()}
|
||||
|
@ -242,8 +273,7 @@ class MascotPopup extends React.Component<Props, State> {
|
|||
|
||||
</View>
|
||||
</Portal>
|
||||
)
|
||||
;
|
||||
);
|
||||
} else
|
||||
return null;
|
||||
|
||||
|
|
|
@ -33,11 +33,7 @@ type Props = {
|
|||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
type State = {
|
||||
mascotDialogVisible: boolean,
|
||||
}
|
||||
|
||||
class GameStartScreen extends React.Component<Props, State> {
|
||||
class GameStartScreen extends React.Component<Props> {
|
||||
|
||||
gridManager: GridManager;
|
||||
scores: Array<number>;
|
||||
|
@ -45,10 +41,6 @@ class GameStartScreen extends React.Component<Props, State> {
|
|||
gameStats: GameStats | null;
|
||||
isHighScore: boolean;
|
||||
|
||||
state = {
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.gameStartShowBanner.key),
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.gridManager = new GridManager(4, 4, props.theme);
|
||||
|
@ -75,11 +67,6 @@ class GameStartScreen extends React.Component<Props, State> {
|
|||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameScores.key, this.scores);
|
||||
}
|
||||
|
||||
hideMascotDialog = () => {
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameStartShowBanner.key, false);
|
||||
this.setState({mascotDialogVisible: false})
|
||||
};
|
||||
|
||||
getPiecesBackground() {
|
||||
let gridList = [];
|
||||
for (let i = 0; i < 18; i++) {
|
||||
|
@ -415,7 +402,7 @@ class GameStartScreen extends React.Component<Props, State> {
|
|||
<CollapsibleScrollView>
|
||||
{this.getMainContent()}
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key}
|
||||
title={i18n.t("screens.game.mascotDialog.title")}
|
||||
message={i18n.t("screens.game.mascotDialog.message")}
|
||||
icon={"gamepad-variant"}
|
||||
|
@ -424,7 +411,6 @@ class GameStartScreen extends React.Component<Props, State> {
|
|||
cancel: {
|
||||
message: i18n.t("screens.game.mascotDialog.button"),
|
||||
icon: "check",
|
||||
onPress: this.hideMascotDialog,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
|
|
|
@ -84,7 +84,6 @@ type Props = {
|
|||
|
||||
type State = {
|
||||
dialogVisible: boolean,
|
||||
mascotDialogVisible: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,9 +111,6 @@ class HomeScreen extends React.Component<Props, State> {
|
|||
});
|
||||
this.state = {
|
||||
dialogVisible: false,
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.homeShowBanner.key)
|
||||
&& !this.isLoggedIn,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,11 +180,6 @@ class HomeScreen extends React.Component<Props, State> {
|
|||
</MaterialHeaderButtons>;
|
||||
};
|
||||
|
||||
hideMascotDialog = () => {
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.homeShowBanner.key, false);
|
||||
this.setState({mascotDialogVisible: false})
|
||||
};
|
||||
|
||||
showDisconnectDialog = () => this.setState({dialogVisible: true});
|
||||
|
||||
hideDisconnectDialog = () => this.setState({dialogVisible: false});
|
||||
|
@ -525,10 +516,7 @@ class HomeScreen extends React.Component<Props, State> {
|
|||
* Callback when pressing the login button on the banner.
|
||||
* This hides the banner and takes the user to the login page.
|
||||
*/
|
||||
onLogin = () => {
|
||||
this.hideMascotDialog();
|
||||
this.props.navigation.navigate("login", {nextScreen: "profile"});
|
||||
}
|
||||
onLogin = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -554,8 +542,9 @@ class HomeScreen extends React.Component<Props, State> {
|
|||
renderListHeaderComponent={this.getListHeader}
|
||||
/>
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
{!this.isLoggedIn
|
||||
? <MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key}
|
||||
title={i18n.t("screens.home.mascotDialog.title")}
|
||||
message={i18n.t("screens.home.mascotDialog.message")}
|
||||
icon={"human-greeting"}
|
||||
|
@ -569,11 +558,10 @@ class HomeScreen extends React.Component<Props, State> {
|
|||
message: i18n.t("screens.home.mascotDialog.later"),
|
||||
icon: "close",
|
||||
color: this.props.theme.colors.warning,
|
||||
onPress: this.hideMascotDialog,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.CUTE}
|
||||
/>
|
||||
/> : null}
|
||||
<AnimatedFAB
|
||||
{...this.props}
|
||||
ref={this.fabRef}
|
||||
|
|
|
@ -26,7 +26,6 @@ type Props = {
|
|||
}
|
||||
|
||||
type State = {
|
||||
mascotDialogVisible: boolean,
|
||||
dialogVisible: boolean,
|
||||
dialogTitle: string,
|
||||
dialogMessage: string,
|
||||
|
@ -143,10 +142,6 @@ class PlanexScreen extends React.Component<Props, State> {
|
|||
props.navigation.setOptions({title: currentGroup.name})
|
||||
}
|
||||
this.state = {
|
||||
mascotDialogVisible:
|
||||
AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.planexShowBanner.key)
|
||||
&& AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key)
|
||||
.toLowerCase() !== 'planex',
|
||||
dialogVisible: false,
|
||||
dialogTitle: "",
|
||||
dialogMessage: "",
|
||||
|
@ -162,24 +157,11 @@ class PlanexScreen extends React.Component<Props, State> {
|
|||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when closing the banner.
|
||||
* This hides the banner and saves to preferences to prevent it from reopening
|
||||
*/
|
||||
onMascotDialogCancel = () => {
|
||||
this.setState({mascotDialogVisible: false});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.planexShowBanner.key, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Callback used when the user clicks on the navigate to settings button.
|
||||
* This will hide the banner and open the SettingsScreen
|
||||
*/
|
||||
onGoToSettings = () => {
|
||||
this.onMascotDialogCancel();
|
||||
this.props.navigation.navigate('settings');
|
||||
};
|
||||
onGoToSettings = () => this.props.navigation.navigate('settings');
|
||||
|
||||
onScreenFocus = () => {
|
||||
this.handleNavigationParams();
|
||||
|
@ -357,8 +339,10 @@ class PlanexScreen extends React.Component<Props, State> {
|
|||
? this.getWebView()
|
||||
: <View style={{height: '100%'}}>{this.getWebView()}</View>}
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
{AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key)
|
||||
.toLowerCase() !== 'planex'
|
||||
? <MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.planexShowBanner.key}
|
||||
title={i18n.t("screens.planex.mascotDialog.title")}
|
||||
message={i18n.t("screens.planex.mascotDialog.message")}
|
||||
icon={"emoticon-kiss"}
|
||||
|
@ -372,11 +356,10 @@ class PlanexScreen extends React.Component<Props, State> {
|
|||
message: i18n.t("screens.planex.mascotDialog.cancel"),
|
||||
icon: "close",
|
||||
color: this.props.theme.colors.warning,
|
||||
onPress: this.onMascotDialogCancel,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.INTELLO}
|
||||
/>
|
||||
/> : null }
|
||||
<AlertDialog
|
||||
visible={this.state.dialogVisible}
|
||||
onDismiss={this.hideDialog}
|
||||
|
|
|
@ -36,7 +36,6 @@ type State = {
|
|||
refreshing: boolean,
|
||||
agendaItems: Object,
|
||||
calendarShowing: boolean,
|
||||
mascotDialogVisible: boolean,
|
||||
};
|
||||
|
||||
const FETCH_URL = "https://www.amicale-insat.fr/api/event/list";
|
||||
|
@ -56,7 +55,6 @@ class PlanningScreen extends React.Component<Props, State> {
|
|||
refreshing: false,
|
||||
agendaItems: {},
|
||||
calendarShowing: false,
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.eventsShowBanner.key)
|
||||
};
|
||||
|
||||
currentDate = getDateOnlyString(getCurrentDateString());
|
||||
|
@ -105,15 +103,6 @@ class PlanningScreen extends React.Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when closing the banner.
|
||||
* This hides the banner and saves to preferences to prevent it from reopening
|
||||
*/
|
||||
onHideMascotDialog = () => {
|
||||
this.setState({mascotDialogVisible: false});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.eventsShowBanner.key, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function used to check if a row has changed
|
||||
*
|
||||
|
@ -250,7 +239,7 @@ class PlanningScreen extends React.Component<Props, State> {
|
|||
onRef={this.onAgendaRef}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
|
||||
title={i18n.t("screens.planning.mascotDialog.title")}
|
||||
message={i18n.t("screens.planning.mascotDialog.message")}
|
||||
icon={"party-popper"}
|
||||
|
@ -259,7 +248,6 @@ class PlanningScreen extends React.Component<Props, State> {
|
|||
cancel: {
|
||||
message: i18n.t("screens.planning.mascotDialog.button"),
|
||||
icon: "check",
|
||||
onPress: this.onHideMascotDialog,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.HAPPY}
|
||||
|
|
|
@ -46,7 +46,6 @@ type State = {
|
|||
refreshing: boolean,
|
||||
modalCurrentDisplayItem: React.Node,
|
||||
machinesWatched: Array<Machine>,
|
||||
mascotDialogVisible: boolean,
|
||||
};
|
||||
|
||||
|
||||
|
@ -67,7 +66,6 @@ class ProxiwashScreen extends React.Component<Props, State> {
|
|||
refreshing: false,
|
||||
modalCurrentDisplayItem: null,
|
||||
machinesWatched: AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key),
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key),
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -84,15 +82,6 @@ class ProxiwashScreen extends React.Component<Props, State> {
|
|||
modalStateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.modal.unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when closing the banner.
|
||||
* This hides the banner and saves to preferences to prevent it from reopening
|
||||
*/
|
||||
onHideMascotDialog = () => {
|
||||
this.setState({mascotDialogVisible: false});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup notification channel for android and add listeners to detect notifications fired
|
||||
*/
|
||||
|
@ -409,7 +398,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
|
|||
updateData={this.state.machinesWatched.length}/>
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key}
|
||||
title={i18n.t("screens.proxiwash.mascotDialog.title")}
|
||||
message={i18n.t("screens.proxiwash.mascotDialog.message")}
|
||||
icon={"information"}
|
||||
|
@ -418,7 +407,6 @@ class ProxiwashScreen extends React.Component<Props, State> {
|
|||
cancel: {
|
||||
message: i18n.t("screens.proxiwash.mascotDialog.ok"),
|
||||
icon: "check",
|
||||
onPress: this.onHideMascotDialog,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
|
|
|
@ -20,11 +20,6 @@ type Props = {
|
|||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
type State = {
|
||||
mascotDialogVisible: boolean,
|
||||
}
|
||||
|
||||
|
||||
export type listItem = {
|
||||
title: string,
|
||||
description: string,
|
||||
|
@ -33,14 +28,10 @@ export type listItem = {
|
|||
}
|
||||
|
||||
|
||||
class ServicesScreen extends React.Component<Props, State> {
|
||||
class ServicesScreen extends React.Component<Props> {
|
||||
|
||||
finalDataset: Array<listItem>
|
||||
|
||||
state = {
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.servicesShowBanner.key),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const services = new ServicesManager(props.navigation);
|
||||
|
@ -53,16 +44,6 @@ class ServicesScreen extends React.Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Callback used when closing the banner.
|
||||
* This hides the banner and saves to preferences to prevent it from reopening
|
||||
*/
|
||||
onHideMascotDialog = () => {
|
||||
this.setState({mascotDialogVisible: false});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.servicesShowBanner.key, false);
|
||||
};
|
||||
|
||||
getAboutButton = () =>
|
||||
<MaterialHeaderButtons>
|
||||
<Item title="information" iconName="information" onPress={this.onAboutPress}/>
|
||||
|
@ -146,7 +127,7 @@ class ServicesScreen extends React.Component<Props, State> {
|
|||
hasTab={true}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
prefKey={AsyncStorageManager.PREFERENCES.servicesShowBanner.key}
|
||||
title={i18n.t("screens.services.mascotDialog.title")}
|
||||
message={i18n.t("screens.services.mascotDialog.message")}
|
||||
icon={"cloud-question"}
|
||||
|
|
Loading…
Reference in a new issue