New source found from dndbeyond.com
This commit is contained in:
@@ -1,25 +1,10 @@
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useContext, ReactNode, MouseEvent, PureComponent } from "react";
|
||||
import clsx from "clsx";
|
||||
import { useContext, ReactNode, PureComponent } from "react";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { DarkBuilderSvg } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
DarkBuilderSvg,
|
||||
FeatureFlagContext,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CharacterCurrencyContract,
|
||||
CharacterNotes,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
@@ -33,23 +18,29 @@ import {
|
||||
SnippetData,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
characterSelectors,
|
||||
BaseCharClass,
|
||||
ContainerUtils,
|
||||
InventoryManager,
|
||||
FormatUtils,
|
||||
CampaignUtils,
|
||||
serviceDataSelectors,
|
||||
PartyInfo,
|
||||
CoinManager,
|
||||
ClassUtils,
|
||||
CharClass,
|
||||
ItemPlan,
|
||||
ItemPlanUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
import { Link } from "~/components/Link";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { TabList } from "~/components/TabList";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import a11yStyles from "~/styles/accessibility.module.css";
|
||||
import { EquipmentOverview } from "~/subApps/sheet/components/Equipment/EquipmentOverview";
|
||||
import { MagicItemPlans } from "~/subApps/sheet/components/MagicItemPlans";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import CurrencyButton from "../../../CharacterSheet/components/CurrencyButton";
|
||||
@@ -60,7 +51,6 @@ import { InventoryManagerContext } from "../../../Shared/managers/InventoryManag
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import EquipmentOverview from "../../components/EquipmentOverview";
|
||||
import Infusions from "../../components/Infusions";
|
||||
import InventoryFilter from "../../components/InventoryFilter";
|
||||
import InventoryTableHeader from "../../components/InventoryTableHeader";
|
||||
@@ -69,6 +59,7 @@ import { sheetAppSelectors } from "../../selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import Attunement from "../Attunement";
|
||||
import Inventory from "../Inventory";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props {
|
||||
showNotes: boolean;
|
||||
@@ -77,22 +68,21 @@ interface Props {
|
||||
inventory: Array<Item>;
|
||||
partyInventory: Array<Item>;
|
||||
creatures: Array<Creature>;
|
||||
currencies: CharacterCurrencyContract | null;
|
||||
notes: CharacterNotes;
|
||||
infusionChoices: Array<InfusionChoice>;
|
||||
weight: number;
|
||||
weightSpeedType: Constants.WeightSpeedTypeEnum;
|
||||
ruleData: RuleData;
|
||||
snippetData: SnippetData;
|
||||
isReadonly: boolean;
|
||||
isMobile: boolean;
|
||||
theme: CharacterTheme;
|
||||
proficiencyBonus: number;
|
||||
classes: Array<BaseCharClass>;
|
||||
classes: Array<CharClass>;
|
||||
inventoryManager: InventoryManager;
|
||||
campaignInfo: PartyInfo | null;
|
||||
coinManager: CoinManager;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
itemPlans: Array<ItemPlan>;
|
||||
maxReplicatedItemsCount: number | null;
|
||||
}
|
||||
interface StateFilterData {
|
||||
filteredInventory: Array<Item>;
|
||||
@@ -105,7 +95,6 @@ interface State {
|
||||
filterData: StateFilterData;
|
||||
filteredEquippedInventory: Array<Item>;
|
||||
shouldShowPartyInventory: number;
|
||||
shouldShowDeleteOnlyInfo: boolean;
|
||||
}
|
||||
|
||||
class Equipment extends PureComponent<Props, State> {
|
||||
@@ -128,7 +117,6 @@ class Equipment extends PureComponent<Props, State> {
|
||||
},
|
||||
filteredEquippedInventory: [],
|
||||
shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on...
|
||||
shouldShowDeleteOnlyInfo: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,16 +145,6 @@ class Equipment extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
handleWeightClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.ENCUMBRANCE);
|
||||
};
|
||||
|
||||
handleCampaignClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
handlePossessionsManage = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
@@ -233,12 +211,26 @@ class Equipment extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
handleItemPlansManagePaneShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.ITEM_PLANS_MANAGE);
|
||||
};
|
||||
|
||||
shouldRenderInfusions = (): boolean => {
|
||||
const { infusionChoices } = this.props;
|
||||
|
||||
return infusionChoices.length > 0;
|
||||
};
|
||||
|
||||
shouldRenderMagicItemPlans = (): boolean => {
|
||||
const { classes } = this.props;
|
||||
|
||||
return classes.some((charClass) =>
|
||||
ClassUtils.getEnablesReplicateMagicItem(charClass)
|
||||
);
|
||||
};
|
||||
|
||||
renderContainer = (
|
||||
container: ContainerManager,
|
||||
inventory: Array<Item>
|
||||
@@ -380,7 +372,7 @@ class Equipment extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderAttunement = (): ReactNode => {
|
||||
const { isMobile, theme } = this.props;
|
||||
const { isMobile } = this.props;
|
||||
|
||||
return (
|
||||
<ContentGroup header="Attunement">
|
||||
@@ -389,6 +381,54 @@ class Equipment extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderMagicItemPlans = (): ReactNode => {
|
||||
const { isReadonly, itemPlans, maxReplicatedItemsCount } = this.props;
|
||||
|
||||
if (!this.shouldRenderMagicItemPlans()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemPlanWithMappingsCount = itemPlans.filter((plan) =>
|
||||
ItemPlanUtils.getItem(plan)
|
||||
).length;
|
||||
|
||||
const headerNode: ReactNode = (
|
||||
<>
|
||||
Magic Item Plans{" "}
|
||||
{maxReplicatedItemsCount !== null && (
|
||||
<span className={styles.replicatedCount}>
|
||||
({itemPlanWithMappingsCount}/{maxReplicatedItemsCount} Replicated)
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
let extraNode: ReactNode = null;
|
||||
if (!isReadonly) {
|
||||
extraNode = (
|
||||
<Button
|
||||
variant="text"
|
||||
themed
|
||||
size="x-small"
|
||||
className="ct-equipment__builder-link-text"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleItemPlansManagePaneShow();
|
||||
}}
|
||||
>
|
||||
Manage Plans
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentGroup header={headerNode} extra={extraNode}>
|
||||
<MagicItemPlans />
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderInfusions = (): ReactNode => {
|
||||
const { infusableChoices } = this.state;
|
||||
const {
|
||||
@@ -421,7 +461,7 @@ class Equipment extends PureComponent<Props, State> {
|
||||
<Link
|
||||
href={builderUrl}
|
||||
className="ct-equipment__builder-link"
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
<DarkBuilderSvg />
|
||||
<span className="ct-equipment__builder-link-text">
|
||||
@@ -490,7 +530,7 @@ class Equipment extends PureComponent<Props, State> {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ct-equipment__content">
|
||||
<div className={styles.tabFilter}>
|
||||
<TabFilter
|
||||
sharedChildren={<InventoryTableHeader showNotes={showNotes} />}
|
||||
filters={[
|
||||
@@ -503,20 +543,16 @@ class Equipment extends PureComponent<Props, State> {
|
||||
{this.renderCharacterContainers()}
|
||||
{this.renderAttunement()}
|
||||
{this.shouldRenderInfusions() && this.renderInfusions()}
|
||||
{this.renderMagicItemPlans()}
|
||||
{this.renderOtherPossessions()}
|
||||
</>
|
||||
) : null}
|
||||
<FeatureFlagContext.Consumer>
|
||||
{({ imsFlag }) => {
|
||||
return imsFlag &&
|
||||
shouldShowPartyInventory === 1 &&
|
||||
inventoryManager.getPartyContainers().length > 0 ? (
|
||||
<ContentGroup header={headerNode}>
|
||||
{this.renderPartyContainers()}
|
||||
</ContentGroup>
|
||||
) : null;
|
||||
}}
|
||||
</FeatureFlagContext.Consumer>
|
||||
{shouldShowPartyInventory === 1 &&
|
||||
inventoryManager.getPartyContainers().length > 0 ? (
|
||||
<ContentGroup header={headerNode}>
|
||||
{this.renderPartyContainers()}
|
||||
</ContentGroup>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -551,6 +587,14 @@ class Equipment extends PureComponent<Props, State> {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(this.shouldRenderMagicItemPlans()
|
||||
? [
|
||||
{
|
||||
label: "Magic Item Plans",
|
||||
content: this.renderMagicItemPlans(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isReadonly
|
||||
? [
|
||||
{
|
||||
@@ -592,126 +636,17 @@ class Equipment extends PureComponent<Props, State> {
|
||||
shouldShowPartyInventory: newValue,
|
||||
});
|
||||
};
|
||||
handleCloseDeleteOnlyInfo = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({
|
||||
shouldShowDeleteOnlyInfo: false,
|
||||
});
|
||||
};
|
||||
handleOpenDeleteOnlyInfo = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({
|
||||
shouldShowDeleteOnlyInfo: true,
|
||||
});
|
||||
};
|
||||
|
||||
renderDeleteOnlyAlertAction = (): ReactNode => {
|
||||
const { shouldShowDeleteOnlyInfo } = this.state;
|
||||
const { campaignInfo } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ThemeButton
|
||||
style="outline"
|
||||
size="medium"
|
||||
onClick={this.handleOpenDeleteOnlyInfo}
|
||||
>
|
||||
More Info
|
||||
</ThemeButton>
|
||||
<Dialog
|
||||
open={shouldShowDeleteOnlyInfo}
|
||||
onClose={this.handleCloseDeleteOnlyInfo}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Delete Only</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Party Inventory sharing isn’t currently enabled for this campaign.
|
||||
Your old items are still here, but you won’t be able to add new
|
||||
items until it’s turned on again.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{campaignInfo ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
component="a"
|
||||
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
|
||||
size="small"
|
||||
>
|
||||
go to campaign
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={this.handleCloseDeleteOnlyInfo}
|
||||
autoFocus
|
||||
size="small"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderOverviewFiltersAndContent = (): ReactNode => {
|
||||
const { filterData, shouldShowPartyInventory } = this.state;
|
||||
const {
|
||||
currencies,
|
||||
weight,
|
||||
weightSpeedType,
|
||||
inventory,
|
||||
ruleData,
|
||||
theme,
|
||||
partyInventory,
|
||||
campaignInfo,
|
||||
inventoryManager,
|
||||
coinManager,
|
||||
} = this.props;
|
||||
|
||||
const cointainerFlagEnabled: boolean = coinManager.canUseCointainers();
|
||||
let coin: CharacterCurrencyContract | null = currencies;
|
||||
if (shouldShowPartyInventory === 1 && campaignInfo) {
|
||||
// We need all the party coin if flag is on, or just Party Equipment coin if not
|
||||
if (cointainerFlagEnabled) {
|
||||
coin = coinManager.getAllPartyCoin();
|
||||
} else {
|
||||
coin = CampaignUtils.getCoin(campaignInfo);
|
||||
}
|
||||
} else if (cointainerFlagEnabled) {
|
||||
// We need to change it to all container currencies
|
||||
coin = coinManager.getAllCharacterCoin();
|
||||
}
|
||||
const { inventory, ruleData, theme, partyInventory } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ct-equipment__overview">
|
||||
<EquipmentOverview
|
||||
shouldShowPartyInventory={shouldShowPartyInventory === 1}
|
||||
currencies={coin}
|
||||
weight={weight}
|
||||
weightSpeedType={weightSpeedType}
|
||||
onCurrencyClick={(evt) =>
|
||||
this.handleCurrencyClick(
|
||||
evt,
|
||||
shouldShowPartyInventory === 1
|
||||
? inventoryManager.getPartyEquipmentContainerDefinitionKey() ??
|
||||
inventoryManager.getCharacterContainerDefinitionKey()
|
||||
: inventoryManager.getCharacterContainerDefinitionKey()
|
||||
)
|
||||
}
|
||||
onWeightClick={this.handleWeightClick}
|
||||
onCampaignClick={this.handleCampaignClick}
|
||||
enableManage={false}
|
||||
isDarkMode={theme.isDarkMode}
|
||||
campaignName={
|
||||
campaignInfo !== null ? CampaignUtils.getName(campaignInfo) : null
|
||||
activeEquipmentTab={
|
||||
shouldShowPartyInventory === 1 ? "party" : "character"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -730,113 +665,39 @@ class Equipment extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderOptinToInventorySharing = (): ReactNode => {
|
||||
handleChangeTab = (id) =>
|
||||
this.setState({
|
||||
shouldShowPartyInventory: id === "party-inventory" ? 1 : 0,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { campaignInfo } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack justifyContent="center">
|
||||
<Stack marginBottom="10px" marginTop="40px">
|
||||
<Typography color="primary" align="center" variant="body2">
|
||||
Want a hand with that loot?
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack marginBottom="30px" alignItems={"center"}>
|
||||
<Typography
|
||||
align="center"
|
||||
variant="body1"
|
||||
sx={{ maxWidth: "320px" }}
|
||||
>
|
||||
Party Inventory makes it easy for your group to keep track of all
|
||||
their shared loot and coin. Add it to your campaign today with a
|
||||
subscription to D&D Beyond!
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
justifyContent="center"
|
||||
>
|
||||
{campaignInfo ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component="a"
|
||||
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
|
||||
size="large"
|
||||
sx={{
|
||||
marginLeft: { xs: 0 },
|
||||
marginRight: { xs: 0 },
|
||||
}}
|
||||
>
|
||||
go to campaign
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button variant="contained" color="primary" size="large">
|
||||
view subscription plans
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={{ display: "flex", justifyContent: "center", marginTop: 51 }}
|
||||
>
|
||||
<div className="ct-item-detail__full-image">
|
||||
<img
|
||||
className="ct-item-detail__full-image-img"
|
||||
src="https://www.dndbeyond.com/avatars/thumbnails/7/120/315/315/636284708068284913.jpeg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { shouldShowPartyInventory } = this.state;
|
||||
const { campaignInfo, inventoryManager } = this.props;
|
||||
|
||||
return (
|
||||
<section className="ct-equipment">
|
||||
<h2 style={visuallyHidden}>Inventory</h2>
|
||||
<FeatureFlagContext.Consumer>
|
||||
{({ imsFlag }) => {
|
||||
return imsFlag && campaignInfo !== null ? (
|
||||
<>
|
||||
<Tabs
|
||||
value={shouldShowPartyInventory}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={this.toggleShouldShowPartyInventory}
|
||||
aria-label="disabled tabs example"
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab label="My Inventory" data-testid="my-inventory" />
|
||||
<Tab label="Party Inventory" data-testid="party-inventory" />
|
||||
</Tabs>
|
||||
{/* TODO: make this go away when last itme is removed... */}
|
||||
{shouldShowPartyInventory &&
|
||||
inventoryManager.isSharingTurnedDeleteOnly() ? (
|
||||
<div role="button" onMouseUp={this.handleOpenDeleteOnlyInfo}>
|
||||
<Alert
|
||||
severity="info"
|
||||
action={this.renderDeleteOnlyAlertAction()}
|
||||
>
|
||||
Party Inventory is currently turned off for this campaign.
|
||||
</Alert>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowPartyInventory &&
|
||||
inventoryManager.isSharingTurnedOff()
|
||||
? this.renderOptinToInventorySharing()
|
||||
: this.renderOverviewFiltersAndContent()}
|
||||
</>
|
||||
) : (
|
||||
this.renderOverviewFiltersAndContent()
|
||||
);
|
||||
}}
|
||||
</FeatureFlagContext.Consumer>
|
||||
<section className={clsx(["ct-equipment", styles.section])}>
|
||||
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
|
||||
{campaignInfo !== null ? (
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
variant="fullwidth"
|
||||
themed={true}
|
||||
tabs={[
|
||||
{
|
||||
label: "My Inventory",
|
||||
content: this.renderOverviewFiltersAndContent(),
|
||||
id: "my-inventory",
|
||||
},
|
||||
{
|
||||
label: "Party Inventory",
|
||||
content: this.renderOverviewFiltersAndContent(),
|
||||
id: "party-inventory",
|
||||
},
|
||||
]}
|
||||
onChangeTab={this.handleChangeTab}
|
||||
/>
|
||||
) : (
|
||||
this.renderOverviewFiltersAndContent()
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -848,19 +709,19 @@ function mapStateToProps(state: SheetAppState) {
|
||||
inventory: rulesEngineSelectors.getInventory(state),
|
||||
partyInventory: rulesEngineSelectors.getPartyInventory(state),
|
||||
creatures: rulesEngineSelectors.getCreatures(state),
|
||||
weight: rulesEngineSelectors.getTotalCarriedWeight(state),
|
||||
weightSpeedType: rulesEngineSelectors.getCurrentCarriedWeightType(state),
|
||||
notes: rulesEngineSelectors.getCharacterNotes(state),
|
||||
infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state),
|
||||
currencies: rulesEngineSelectors.getCurrencies(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
snippetData: rulesEngineSelectors.getSnippetData(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
isMobile: appEnvSelectors.getIsMobile(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
classes: characterSelectors.getClasses(state),
|
||||
classes: rulesEngineSelectors.getClasses(state),
|
||||
campaignInfo: serviceDataSelectors.getPartyInfo(state),
|
||||
itemPlans: rulesEngineSelectors.getAvailableItemPlans(state),
|
||||
maxReplicatedItemsCount:
|
||||
rulesEngineSelectors.getMaxReplicatedItemsCount(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user