nitro-v36-with-renderer-v36
/
src /
components /
inventory /
views /
furniture /
InventoryTradeView.tsx
InventoryTradeView.tsx 19.3 Ko
Télécharger le fichier
import { IObjectData, TradingListAddItemComposer, TradingListAddItemsComposer } from '@octane/renderer';
import { FC, useEffect, useState } from 'react';
import { FaChevronLeft, FaChevronRight, FaLock, FaUnlock } from 'react-icons/fa';
import {
FurniCategory,
GetConfigurationValue,
GroupItem,
getGuildFurniType,
getTradeOfferSummary,
IFurnitureItem,
LocalizeText,
localizeWithFallback,
NotificationAlertType,
isTradeOfferingCredits,
SendMessageComposer,
TradeState
} from '../../../../api';
import { AutoGrid, BaseButton, Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../../common';
import { useInventoryTrade, useNotification } from '../../../../hooks';
import { InventoryFurnitureSearchView } from './InventoryFurnitureSearchView';
import { NumberInput } from '../../../../common/NumberInput';
import { clampTradeQuantity } from '../../../../api/inventory/TradeQuantity';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
interface InventoryTradeViewProps {
cancelTrade: () => void;
}
const MAX_ITEMS_TO_TRADE: number = 9;
export const InventoryTradeView: FC<InventoryTradeViewProps> = (props) => {
const { cancelTrade = null } = props;
const [groupItem, setGroupItem] = useState<GroupItem>(null);
const [ownGroupItem, setOwnGroupItem] = useState<GroupItem>(null);
const [otherGroupItem, setOtherGroupItem] = useState<GroupItem>(null);
const [filteredGroupItems, setFilteredGroupItems] = useState<GroupItem[]>(null);
const [countdownTick, setCountdownTick] = useState(3);
const [quantity, setQuantity] = useState<number>(1);
const {
ownUser = null,
otherUser = null,
groupItems = [],
tradeState = TradeState.TRADING_STATE_READY,
progressTrade = null,
removeItem = null,
setTradeState = null
} = useInventoryTrade();
const { simpleAlert = null } = useNotification();
const canTradeItem = (isWallItem: boolean, spriteId: number, category: number, groupable: boolean, stuffData: IObjectData) => {
if (!ownUser || ownUser.accepts || !ownUser.userItems) return false;
if (ownUser.userItems.length < MAX_ITEMS_TO_TRADE) return true;
if (!groupable) return false;
let type = spriteId.toString();
if (category === FurniCategory.POSTER) {
type = type + 'poster' + stuffData.getLegacyString();
} else {
if (category === FurniCategory.GUILD_FURNI) {
type = getGuildFurniType(spriteId, stuffData);
} else {
type = (isWallItem ? 'I' : 'S') + type;
}
}
return !!ownUser.userItems.getValue(type);
};
const attemptItemOffer = (count: number) => {
if (!groupItem) return;
const tradeItems = groupItem.getTradeItems(count);
if (!tradeItems || !tradeItems.length) return;
let coreItem: IFurnitureItem = null;
const itemIds: number[] = [];
for (const item of tradeItems) {
itemIds.push(item.id);
if (!coreItem) coreItem = item;
}
const ownItemCount = ownUser.userItems.length;
if (ownItemCount + itemIds.length <= 1500) {
if (!coreItem.isGroupable && itemIds.length) {
SendMessageComposer(new TradingListAddItemComposer(itemIds.pop()));
} else {
const tradeIds: number[] = [];
for (const itemId of itemIds) {
if (canTradeItem(coreItem.isWallItem, coreItem.type, coreItem.category, coreItem.isGroupable, coreItem.stuffData)) {
tradeIds.push(itemId);
}
}
if (tradeIds.length) {
if (tradeIds.length === 1) {
SendMessageComposer(new TradingListAddItemComposer(tradeIds.pop()));
} else {
SendMessageComposer(new TradingListAddItemsComposer(...tradeIds));
}
}
}
} else {
simpleAlert(
LocalizeText('trading.items.too_many_items.desc'),
NotificationAlertType.DEFAULT,
null,
null,
LocalizeText('trading.items.too_many_items.title')
);
}
};
const getLockIcon = (accepts: boolean) => {
if (accepts) {
return <FaLock className="text-success fa-icon" />;
} else {
return <FaUnlock className="text-danger fa-icon" />;
}
};
const changeCount = (totalItemCount: number) => {
// Offer the amount once checked: the raw field could be NaN, which offered the whole stack.
const count = clampTradeQuantity(quantity, totalItemCount);
if (count <= 0) return;
setQuantity(count);
attemptItemOffer(count);
};
useEffect(() => {
setQuantity(1);
}, [groupItem]);
useEffect(() => {
if (tradeState !== TradeState.TRADING_STATE_COUNTDOWN) return;
setCountdownTick(3);
const interval = setInterval(() => {
setCountdownTick((prevValue) => {
const newValue = prevValue - 1;
if (newValue === 0) clearInterval(interval);
return newValue;
});
}, 1000);
return () => clearInterval(interval);
}, [tradeState, setTradeState]);
useEffect(() => {
if (countdownTick !== 0) return;
setTradeState(TradeState.TRADING_STATE_CONFIRMING);
}, [countdownTick, setTradeState]);
if (tradeState === TradeState.TRADING_STATE_READY || !ownUser || !otherUser) return null;
// The server says when an account may not trade; without this the trade just never completes.
let tradeBlockedWarning: string = null;
if (!ownUser.canTrade && !otherUser.canTrade)
tradeBlockedWarning = localizeWithFallback('inventory.trading.warning.both_accounts_disabled', 'Neither of you can trade right now.');
else if (!ownUser.canTrade)
tradeBlockedWarning = localizeWithFallback('inventory.trading.warning.own_account_disabled', 'Your account can not trade right now.');
else if (!otherUser.canTrade)
tradeBlockedWarning = localizeWithFallback(
'inventory.trading.warning.others_account_disabled',
`${otherUser.userName} can not trade right now.`,
['otherusername'],
[otherUser.userName]
);
// AIR TradingView.showOfferInfo / updateActionState: with trading.warning.enabled each side shows its item count and
// credit value, and an orange line warns that credit furni turns into wallet credits once either side offers some.
const tradeWarningEnabled = GetConfigurationValue<boolean>('trading.warning.enabled', true) !== false;
const offerSummary = (user: typeof ownUser) => {
if (!tradeWarningEnabled) return null;
const summary = getTradeOfferSummary(user);
return (
<NativeTextTree>
<div className="octane-trade-offer-summary flex flex-col text-xs text-black">
<span className="is-items">{summary.items}</span>
<span className="is-credits">{summary.credits}</span>
</div>
</NativeTextTree>
);
};
const creditWarning =
tradeWarningEnabled && isTradeOfferingCredits(ownUser, otherUser)
? localizeWithFallback('inventory.trading.warning.credits', 'Credit furni will be converted to credits in your purse')
: null;
return (
<NativeTextTree>
<>
{tradeBlockedWarning && <div className="mb-1 rounded bg-[#ffe6b3] px-2 py-1 text-xs text-black">{tradeBlockedWarning}</div>}
<Grid>
<Column overflow="hidden" size={4}>
<InventoryFurnitureSearchView groupItems={groupItems} setGroupItems={setFilteredGroupItems} />
<Flex column fullHeight gap={2} justifyContent="between" overflow="hidden">
<AutoGrid columnCount={3}>
{filteredGroupItems &&
filteredGroupItems.length > 0 &&
filteredGroupItems.map((item, index) => {
const count = item.getUnlockedCount();
return (
<LayoutGridItem
key={index}
className={!count ? 'opacity-50 ' : ''}
component="div"
itemActive={groupItem === item}
itemCount={count}
itemImage={item.iconUrl}
itemUniqueNumber={item.stuffData.uniqueNumber}
onClick={(event) => count && setGroupItem(item)}
onDoubleClick={(event) => attemptItemOffer(1)}
>
{count > 0 && groupItem === item && (
<Button
className="bottom-1 inset-e-1 z-[5] min-h-0 text-[8px] px-[2px] py-[1px]"
position="absolute"
variant="success"
onClick={(event) => attemptItemOffer(1)}
>
<FaChevronRight className="fa-icon" />
</Button>
)}
</LayoutGridItem>
);
})}
</AutoGrid>
<Column alignItems="end" gap={1}>
<Grid overflow="hidden">
<Column overflow="hidden" size={6}>
<NumberInput
className="w-[49px] min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm"
disabled={!groupItem}
placeholder={LocalizeText('catalog.bundlewidget.spinner.select.amount')}
value={quantity}
onChange={(event) => setQuantity(event.target.valueAsNumber)}
/>
</Column>
<Column overflow="hidden" size={6}>
<Button className="octane-trade-button" disabled={!groupItem} variant="secondary" onClick={(event) => changeCount(groupItem.getUnlockedCount())}>
{LocalizeText('inventory.trading.areoffering')}
</Button>
</Column>
</Grid>
<div className="badge bg-muted w-full">{groupItem ? groupItem.name : LocalizeText('catalog_selectproduct')}</div>
</Column>
</Flex>
</Column>
<Column overflow="hidden" size={8}>
<Grid overflow="hidden">
<Column overflow="hidden" size={6}>
<div className="flex justify-between items-center">
<Text className="octane-trade-offer-title">
<span className="octane-trade-offer-user">{LocalizeText('inventory.trading.you')}</span>{' '}
{LocalizeText('inventory.trading.areoffering')}:
</Text>
{getLockIcon(ownUser.accepts)}
</div>
<AutoGrid columnCount={3}>
{Array.from(Array(MAX_ITEMS_TO_TRADE), (e, i) => {
const item = ownUser.userItems.getWithIndex(i) || null;
if (!item) return <LayoutGridItem key={i} />;
return (
<LayoutGridItem
key={i}
component="div"
itemActive={ownGroupItem === item}
itemCount={item.getTotalCount()}
itemImage={item.iconUrl}
itemUniqueNumber={item.stuffData.uniqueNumber}
onClick={(event) => setOwnGroupItem(item)}
onDoubleClick={(event) => removeItem(item)}
>
{ownGroupItem === item && (
<Button
className="bottom-1 inset-s-1 z-[5] min-h-0 text-[8px] px-[2px] py-[1px]"
position="absolute"
variant="danger"
onClick={(event) => removeItem(item)}
>
<FaChevronLeft className="fa-icon" />
</Button>
)}
</LayoutGridItem>
);
})}
</AutoGrid>
<div className="badge bg-muted w-full">{ownGroupItem ? ownGroupItem.name : LocalizeText('catalog_selectproduct')}</div>
{offerSummary(ownUser)}
</Column>
<Column overflow="hidden" size={6}>
<div className="flex justify-between items-center">
<Text className="octane-trade-offer-title">
<span className="octane-trade-offer-user">{otherUser.userName}</span> {LocalizeText('inventory.trading.isoffering')}:
</Text>
{getLockIcon(otherUser.accepts)}
</div>
<AutoGrid columnCount={3}>
{Array.from(Array(MAX_ITEMS_TO_TRADE), (e, i) => {
const item = otherUser.userItems.getWithIndex(i) || null;
if (!item) return <LayoutGridItem key={i} />;
return (
<LayoutGridItem
key={i}
aria-pressed={otherGroupItem === item}
component={BaseButton}
itemActive={otherGroupItem === item}
itemCount={item.getTotalCount()}
itemImage={item.iconUrl}
itemUniqueNumber={item.stuffData.uniqueNumber}
onClick={(event) => setOtherGroupItem(item)}
/>
);
})}
</AutoGrid>
<div className="badge bg-muted w-full">{otherGroupItem ? otherGroupItem.name : LocalizeText('catalog_selectproduct')}</div>
{offerSummary(otherUser)}
</Column>
</Grid>
{creditWarning && (
<div
className="octane-trade-credit-warning mb-1 rounded border border-[#fc9228] bg-[#fc9228] px-[5px] py-[5px] text-xs text-black"
role="status"
>
{creditWarning}
</div>
)}
<div className="flex grow! justify-between">
<Button className="octane-trade-button" variant="secondary" onClick={cancelTrade}>
{LocalizeText('generic.cancel')}
</Button>
{tradeState === TradeState.TRADING_STATE_READY && (
<Button className="octane-trade-button" disabled={!ownUser.itemCount && !otherUser.itemCount} variant="secondary" onClick={progressTrade}>
{LocalizeText('inventory.trading.accept')}
</Button>
)}
{tradeState === TradeState.TRADING_STATE_RUNNING && (
<Button className="octane-trade-button" disabled={!ownUser.itemCount && !otherUser.itemCount} variant="secondary" onClick={progressTrade}>
{LocalizeText(ownUser.accepts ? 'inventory.trading.modify' : 'inventory.trading.accept')}
</Button>
)}
{tradeState === TradeState.TRADING_STATE_COUNTDOWN && (
<Button className="octane-trade-button" disabled variant="secondary">
{LocalizeText('inventory.trading.countdown', ['counter'], [countdownTick.toString()])}
</Button>
)}
{tradeState === TradeState.TRADING_STATE_CONFIRMING && (
<Button className="octane-trade-button" variant="secondary" onClick={progressTrade}>
{LocalizeText('inventory.trading.button.restore')}
</Button>
)}
{tradeState === TradeState.TRADING_STATE_CONFIRMED && (
<Button className="octane-trade-button" variant="secondary">{LocalizeText('inventory.trading.info.waiting')}</Button>
)}
</div>
</Column>
</Grid>
</>
</NativeTextTree>
);
};
