mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge branch 'duckietm:main' into main
This commit is contained in:
@@ -11,6 +11,7 @@ export interface IRoomData
|
||||
tags: string[];
|
||||
tradeState: number;
|
||||
allowWalkthrough: boolean;
|
||||
allowUnderpass: boolean;
|
||||
lockState: number;
|
||||
password: string;
|
||||
allowPets: boolean;
|
||||
|
||||
@@ -8,6 +8,7 @@ const CURRENT_WINDOWS: HTMLElement[] = [];
|
||||
const POS_MEMORY: Map<Key, { x: number, y: number }> = new Map();
|
||||
const BOUNDS_THRESHOLD_TOP: number = 0;
|
||||
const BOUNDS_THRESHOLD_LEFT: number = 0;
|
||||
const DRAG_OUTSIDE_PERCENT: number = 0.80;
|
||||
|
||||
export interface DraggableWindowProps {
|
||||
uniqueKey?: Key;
|
||||
@@ -80,8 +81,11 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const clampedX = Math.max(BOUNDS_THRESHOLD_LEFT, Math.min(newX, viewportWidth - windowWidth));
|
||||
const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight));
|
||||
const maxOutX = windowWidth * DRAG_OUTSIDE_PERCENT;
|
||||
const maxOutY = windowHeight * DRAG_OUTSIDE_PERCENT;
|
||||
|
||||
const clampedX = Math.max(-maxOutX, Math.min(newX, viewportWidth - windowWidth + maxOutX));
|
||||
const clampedY = Math.max(-maxOutY, Math.min(newY, viewportHeight - windowHeight + maxOutY));
|
||||
|
||||
return { x: clampedX, y: clampedY };
|
||||
}, []);
|
||||
|
||||
@@ -67,11 +67,20 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
{
|
||||
if(event.badgeId !== badgeCode) return;
|
||||
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
|
||||
|
||||
console.log ('boe');
|
||||
if(isGroup)
|
||||
{
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
element.onload = () => setImageElement(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => setImageElement(img);
|
||||
img.src = badgeUrl;
|
||||
}
|
||||
|
||||
didSetBadge = true;
|
||||
|
||||
@@ -84,13 +93,23 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
|
||||
if(texture && !didSetBadge)
|
||||
{
|
||||
(async () =>
|
||||
if(isGroup)
|
||||
{
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(texture));
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(texture));
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
})();
|
||||
element.onload = () => setImageElement(element);
|
||||
})();
|
||||
}
|
||||
else
|
||||
{
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => setImageElement(img);
|
||||
img.src = badgeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
|
||||
@@ -85,6 +85,7 @@ export const MainView: FC<{}> = props =>
|
||||
<AnimatePresence>
|
||||
{ landingViewVisible &&
|
||||
<motion.div
|
||||
className="w-full h-full"
|
||||
initial={ { opacity: 0 }}
|
||||
animate={ { opacity: 1 }}
|
||||
exit={ { opacity: 0 }}>
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
return (
|
||||
<DraggableWindow uniqueKey="nitro-camera-capture">
|
||||
<Column center className="relative" gap={ 0 }>
|
||||
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[320px] h-[320px]" src={ selectedPicture.imageUrl } /> }
|
||||
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[325px] h-[325px]" src={ selectedPicture.imageUrl } /> }
|
||||
<div className="relative w-[340px] h-[462px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-position-[-1px_-1px] drag-handler">
|
||||
<div className="absolute top-[8px] right-[8px] rounded-[.25rem] [box-shadow:0_0_0_1.5px_#fff] border-2 border-[solid] border-[#921911] bg-[repeating-linear-gradient(rgb(245,80,65),rgb(245,80,65)_50%,rgb(194,48,39)_50%,rgb(194,48,39)_100%)] cursor-pointer leading-none px-[3px] py-px" onClick={ onClose }>
|
||||
<FaTimes className="fa-icon" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, NitroTexture, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
|
||||
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
|
||||
import { Button, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
|
||||
import { CameraWidgetEffectListView } from './effect-list';
|
||||
|
||||
export interface CameraWidgetEditorViewProps {
|
||||
@@ -23,10 +23,18 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
|
||||
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
|
||||
const [ isZoomed, setIsZoomed ] = useState(false);
|
||||
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>('');
|
||||
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>(picture?.imageUrl ?? '');
|
||||
const [ stableTexture, setStableTexture ] = useState<NitroTexture>(null);
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
const requestIdRef = useRef<number>(0);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const img = new Image();
|
||||
img.onload = () => setStableTexture(NitroTexture.from(img));
|
||||
img.src = picture.imageUrl;
|
||||
}, [ picture ]);
|
||||
|
||||
const getColorMatrixEffects = useMemo(() => {
|
||||
return availableEffects.filter(effect => effect.colorMatrix);
|
||||
}, [ availableEffects ]);
|
||||
@@ -104,16 +112,27 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
return;
|
||||
}
|
||||
case 'clear_effects':
|
||||
setSelectedEffectName(null);
|
||||
setSelectedEffects([]);
|
||||
onCancel();
|
||||
return;
|
||||
case 'download': {
|
||||
(async () => {
|
||||
const image = new Image();
|
||||
image.src = currentPictureUrl;
|
||||
const newWindow = window.open('');
|
||||
newWindow.document.write(image.outerHTML);
|
||||
})();
|
||||
if(!currentPictureUrl) return;
|
||||
|
||||
const parts = currentPictureUrl.split(',');
|
||||
const mime = parts[0].match(/:(.*?);/)?.[1] || 'image/png';
|
||||
const binary = atob(parts[1]);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for(let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
const blob = new Blob([ bytes ], { type: mime });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
const w = window.open('', '_blank');
|
||||
if(w)
|
||||
{
|
||||
w.document.title = 'camera_photo.png';
|
||||
w.document.body.style.margin = '0';
|
||||
w.document.body.innerHTML = `<img src="${ blobUrl }" style="max-width:100%"/>`;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case 'zoom':
|
||||
@@ -123,25 +142,29 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!stableTexture) return;
|
||||
|
||||
const processThumbnails = async () => {
|
||||
const renderedEffects = await Promise.all(
|
||||
availableEffects.map(effect =>
|
||||
GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
||||
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
||||
)
|
||||
);
|
||||
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
|
||||
};
|
||||
processThumbnails();
|
||||
}, [ picture, availableEffects ]);
|
||||
}, [ stableTexture, availableEffects ]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!stableTexture) return;
|
||||
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const id = ++requestIdRef.current;
|
||||
|
||||
GetRoomCameraWidgetManager()
|
||||
.applyEffects(picture.texture, selectedEffects, false)
|
||||
.applyEffects(stableTexture, selectedEffects, false)
|
||||
.then(imageElement => {
|
||||
if (id !== requestIdRef.current) return;
|
||||
setCurrentPictureUrl(imageElement.src);
|
||||
@@ -152,7 +175,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
return () => {
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
};
|
||||
}, [ picture, selectedEffects ]);
|
||||
}, [ stableTexture, selectedEffects ]);
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[600px] h-[500px]">
|
||||
@@ -177,16 +200,14 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
</Column>
|
||||
<Column justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<Column center>
|
||||
<LayoutImage
|
||||
style={{
|
||||
width: '320px',
|
||||
height: '320px',
|
||||
backgroundImage: `url(${currentPictureUrl})`,
|
||||
backgroundPosition: isZoomed ? 'center' : 'top left',
|
||||
backgroundSize: isZoomed ? 'contain' : 'auto', // Zoom only affects display
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
/>
|
||||
<div className="w-[325px] h-[325px] overflow-hidden">
|
||||
{ currentPictureUrl && <img
|
||||
alt=""
|
||||
src={ currentPictureUrl }
|
||||
className="w-[325px] h-[325px] [image-rendering:pixelated]"
|
||||
style={ isZoomed ? { transform: 'scale(2)', transformOrigin: 'center' } : undefined }
|
||||
/> }
|
||||
</div>
|
||||
{ selectedEffectName && (
|
||||
<Column center fullWidth gap={ 1 }>
|
||||
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchasedEvent } from '../..
|
||||
import { useCatalog, useFriends, useMessageEvent, useUiEvent } from '../../../../hooks';
|
||||
import { classNames } from '../../../../layout';
|
||||
|
||||
let isBuyingGift = false;
|
||||
|
||||
export const CatalogGiftView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState<boolean>(false);
|
||||
@@ -32,6 +34,7 @@ export const CatalogGiftView: FC<{}> = props =>
|
||||
|
||||
const onClose = useCallback(() =>
|
||||
{
|
||||
isBuyingGift = false;
|
||||
setIsVisible(false);
|
||||
setPageId(0);
|
||||
setOfferId(0);
|
||||
@@ -122,6 +125,10 @@ export const CatalogGiftView: FC<{}> = props =>
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBuyingGift) return;
|
||||
|
||||
isBuyingGift = true;
|
||||
|
||||
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
|
||||
return;
|
||||
}
|
||||
@@ -136,6 +143,7 @@ export const CatalogGiftView: FC<{}> = props =>
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
isBuyingGift = false;
|
||||
onClose();
|
||||
return;
|
||||
case CatalogEvent.INIT_GIFT:
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../..
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
let isPurchasingAd = false;
|
||||
|
||||
export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
@@ -45,6 +47,10 @@ export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
const purchaseAd = () =>
|
||||
{
|
||||
if(isPurchasingAd) return;
|
||||
|
||||
isPurchasingAd = true;
|
||||
|
||||
const pageId = page.pageId;
|
||||
const offerId = page.offers.length >= 1 ? page.offers[0].offerId : -1;
|
||||
const flatId = roomId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CatalogPurchaseState, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
@@ -13,15 +13,18 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const { purse = null, getCurrencyAmount = null } = usePurse();
|
||||
const { clubOffers = null } = catalogOptions;
|
||||
const isPurchasingRef = useRef<boolean>(false);
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
}
|
||||
@@ -83,8 +86,9 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
const purchaseSubscription = useCallback(() =>
|
||||
{
|
||||
if(!pendingOffer) return;
|
||||
if(!pendingOffer || isPurchasingRef.current) return;
|
||||
|
||||
isPurchasingRef.current = true;
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
|
||||
}, [ pendingOffer, currentPage ]);
|
||||
|
||||
+15
-1
@@ -1,5 +1,5 @@
|
||||
import { CancelMarketplaceOfferMessageComposer, GetMarketplaceOwnOffersMessageComposer, MarketplaceCancelOfferResultEvent, MarketplaceOwnOffersEvent, RedeemMarketplaceOfferCreditsMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { LocalizeText, MarketplaceOfferData, MarketPlaceOfferState, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Text } from '../../../../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../../../../hooks';
|
||||
@@ -11,6 +11,8 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
|
||||
const [ creditsWaiting, setCreditsWaiting ] = useState(0);
|
||||
const [ offers, setOffers ] = useState<MarketplaceOfferData[]>([]);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const isRedeemingRef = useRef<boolean>(false);
|
||||
const pendingCancelsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
useMessageEvent<MarketplaceOwnOffersEvent>(MarketplaceOwnOffersEvent, event =>
|
||||
{
|
||||
@@ -54,6 +56,10 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
|
||||
|
||||
const redeemSoldOffers = useCallback(() =>
|
||||
{
|
||||
if(isRedeemingRef.current) return;
|
||||
|
||||
isRedeemingRef.current = true;
|
||||
|
||||
setOffers(prevValue =>
|
||||
{
|
||||
const idsToDelete = soldOffers.map(value => value.offerId);
|
||||
@@ -62,11 +68,19 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
|
||||
});
|
||||
|
||||
SendMessageComposer(new RedeemMarketplaceOfferCreditsMessageComposer());
|
||||
|
||||
setTimeout(() => isRedeemingRef.current = false, 3000);
|
||||
}, [ soldOffers ]);
|
||||
|
||||
const takeItemBack = (offerData: MarketplaceOfferData) =>
|
||||
{
|
||||
if(pendingCancelsRef.current.has(offerData.offerId)) return;
|
||||
|
||||
pendingCancelsRef.current.add(offerData.offerId);
|
||||
|
||||
SendMessageComposer(new CancelMarketplaceOfferMessageComposer(offerData.offerId));
|
||||
|
||||
setTimeout(() => pendingCancelsRef.current.delete(offerData.offerId), 2000);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
import { BuyMarketplaceOfferMessageComposer, GetMarketplaceOffersMessageComposer, MarketplaceBuyOfferResultEvent, MarketPlaceOffersEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceOfferData, MarketplaceSearchType, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Text } from '../../../../../../common';
|
||||
import { useMessageEvent, useNotification, usePurse } from '../../../../../../hooks';
|
||||
@@ -23,6 +23,7 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
|
||||
const [ lastSearch, setLastSearch ] = useState<IMarketplaceSearchOptions>({ minPrice: -1, maxPrice: -1, query: '', type: 3 });
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const { simpleAlert = null, showConfirm = null } = useNotification();
|
||||
const isBuyingRef = useRef<boolean>(false);
|
||||
|
||||
const requestOffers = useCallback((options: IMarketplaceSearchOptions) =>
|
||||
{
|
||||
@@ -56,6 +57,9 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
|
||||
|
||||
showConfirm(LocalizeText('catalog.marketplace.confirm_header'), () =>
|
||||
{
|
||||
if(isBuyingRef.current) return;
|
||||
|
||||
isBuyingRef.current = true;
|
||||
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(offerId));
|
||||
},
|
||||
null, null, null, LocalizeText('catalog.marketplace.confirm_title'));
|
||||
@@ -83,6 +87,8 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
isBuyingRef.current = false;
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
switch(parser.result)
|
||||
|
||||
@@ -6,6 +6,8 @@ import { CatalogPostMarketplaceOfferEvent } from '../../../../../../events';
|
||||
import { useCatalog, useMessageEvent, useNotification, useUiEvent } from '../../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../../layout';
|
||||
|
||||
let isPostingMarketplaceOffer = false;
|
||||
|
||||
export const MarketplacePostOfferView: FC<{}> = props =>
|
||||
{
|
||||
const [ item, setItem ] = useState<FurnitureItem>(null);
|
||||
@@ -65,10 +67,15 @@ export const MarketplacePostOfferView: FC<{}> = props =>
|
||||
|
||||
const postItem = () =>
|
||||
{
|
||||
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice)) return;
|
||||
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice) || isPostingMarketplaceOffer) return;
|
||||
|
||||
showConfirm(LocalizeText('inventory.marketplace.confirm_offer.info', [ 'furniname', 'price' ], [ getFurniTitle, askingPrice.toString() ]), () =>
|
||||
{
|
||||
if(isPostingMarketplaceOffer) return;
|
||||
|
||||
isPostingMarketplaceOffer = true;
|
||||
setTimeout(() => isPostingMarketplaceOffer = false, 5000);
|
||||
|
||||
SendMessageComposer(new MakeOfferMessageComposer(askingPrice, item.isWallItem ? 2 : 1, item.id));
|
||||
setItem(null);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { VipGiftItem } from './VipGiftItemView';
|
||||
|
||||
let isSelectingGift = false;
|
||||
|
||||
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { purse = null } = usePurse();
|
||||
@@ -30,6 +32,10 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
showConfirm(LocalizeText('catalog.club_gift.confirm'), () =>
|
||||
{
|
||||
if(isSelectingGift) return;
|
||||
|
||||
isSelectingGift = true;
|
||||
|
||||
SendMessageComposer(new SelectClubGiftComposer(localizationId));
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
@@ -38,6 +44,8 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
return { ...prevValue };
|
||||
});
|
||||
|
||||
setTimeout(() => isSelectingGift = false, 5000);
|
||||
}, null);
|
||||
}, [ setCatalogOptions, showConfirm ]);
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ interface CatalogPurchaseWidgetViewProps
|
||||
purchaseCallback?: () => void;
|
||||
}
|
||||
|
||||
let isPurchasingCatalogItem = false;
|
||||
|
||||
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
|
||||
{
|
||||
const { noGiftOption = false, purchaseCallback = null } = props;
|
||||
@@ -25,15 +27,19 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
isPurchasingCatalogItem = false;
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
isPurchasingCatalogItem = false;
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED:
|
||||
isPurchasingCatalogItem = false;
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
case CatalogPurchaseSoldOutEvent.SOLD_OUT:
|
||||
isPurchasingCatalogItem = false;
|
||||
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +68,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
|
||||
const purchase = (isGift: boolean = false) =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
if(!currentOffer || isPurchasingCatalogItem) return;
|
||||
|
||||
if(GetClubMemberLevel() < currentOffer.clubLevel)
|
||||
{
|
||||
@@ -78,6 +84,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
return;
|
||||
}
|
||||
|
||||
isPurchasingCatalogItem = true;
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
|
||||
if(purchaseCallback)
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FriendlyTime, GetConfigurationValue, LocalizeText, SendMessageComposer
|
||||
import { Button, Column, Flex, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { usePurse } from '../../../../hooks';
|
||||
|
||||
let isBuyingOffer = false;
|
||||
|
||||
export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Dispatch<SetStateAction<boolean>> }) =>
|
||||
{
|
||||
const { offer = null, setOpen = null } = props;
|
||||
@@ -37,8 +39,14 @@ export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Disp
|
||||
|
||||
const buyOffer = () =>
|
||||
{
|
||||
if(isBuyingOffer) return;
|
||||
|
||||
isBuyingOffer = true;
|
||||
|
||||
SendMessageComposer(new PurchaseTargetedOfferComposer(offer.id, amount));
|
||||
SendMessageComposer(new GetTargetedOfferComposer());
|
||||
|
||||
setTimeout(() => isBuyingOffer = false, 5000);
|
||||
};
|
||||
|
||||
if(!offer) return;
|
||||
|
||||
@@ -15,6 +15,8 @@ interface GroupCreatorViewProps
|
||||
|
||||
const TABS: number[] = [ 1, 2, 3, 4 ];
|
||||
|
||||
let isBuyingGroup = false;
|
||||
|
||||
export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
|
||||
{
|
||||
const { onClose = null } = props;
|
||||
@@ -34,7 +36,10 @@ export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
|
||||
|
||||
const buyGroup = () =>
|
||||
{
|
||||
if(!groupData) return;
|
||||
if(!groupData || isBuyingGroup) return;
|
||||
|
||||
isBuyingGroup = true;
|
||||
setTimeout(() => isBuyingGroup = false, 5000);
|
||||
|
||||
const badge = [];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, GroupAdminGiveComposer, GroupAdminTakeComposer, GroupConfirmMemberRemoveEvent, GroupConfirmRemoveMemberComposer, GroupMemberParser, GroupMembersComposer, GroupMembersEvent, GroupMembershipAcceptComposer, GroupMembershipDeclineComposer, GroupMembersParser, GroupRank, GroupRemoveMemberComposer, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { GetUserProfile, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
@@ -16,6 +16,7 @@ export const GroupMembersView: FC<{}> = props =>
|
||||
const [ searchQuery, setSearchQuery ] = useState<string>('');
|
||||
const [ removingMemberName, setRemovingMemberName ] = useState<string>(null);
|
||||
const { showConfirm = null } = useNotification();
|
||||
const pendingActionsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const getRankDescription = (member: GroupMemberParser) =>
|
||||
{
|
||||
@@ -42,6 +43,11 @@ export const GroupMembersView: FC<{}> = props =>
|
||||
{
|
||||
if(!membersData.admin || (member.rank === GroupRank.OWNER)) return;
|
||||
|
||||
const key = `admin_${member.id}`;
|
||||
if(pendingActionsRef.current.has(key)) return;
|
||||
pendingActionsRef.current.add(key);
|
||||
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
|
||||
|
||||
if(member.rank !== GroupRank.ADMIN) SendMessageComposer(new GroupAdminGiveComposer(membersData.groupId, member.id));
|
||||
else SendMessageComposer(new GroupAdminTakeComposer(membersData.groupId, member.id));
|
||||
|
||||
@@ -52,6 +58,11 @@ export const GroupMembersView: FC<{}> = props =>
|
||||
{
|
||||
if(!membersData.admin || (member.rank !== GroupRank.REQUESTED)) return;
|
||||
|
||||
const key = `accept_${member.id}`;
|
||||
if(pendingActionsRef.current.has(key)) return;
|
||||
pendingActionsRef.current.add(key);
|
||||
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
|
||||
|
||||
SendMessageComposer(new GroupMembershipAcceptComposer(membersData.groupId, member.id));
|
||||
|
||||
refreshMembers();
|
||||
@@ -61,6 +72,11 @@ export const GroupMembersView: FC<{}> = props =>
|
||||
{
|
||||
if(!membersData.admin) return;
|
||||
|
||||
const key = `remove_${member.id}`;
|
||||
if(pendingActionsRef.current.has(key)) return;
|
||||
pendingActionsRef.current.add(key);
|
||||
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
|
||||
|
||||
if(member.rank === GroupRank.REQUESTED)
|
||||
{
|
||||
SendMessageComposer(new GroupMembershipDeclineComposer(membersData.groupId, member.id));
|
||||
|
||||
@@ -83,27 +83,36 @@ export const HotelView: FC<{}> = props =>
|
||||
|
||||
if(!container) return;
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight - 55;
|
||||
|
||||
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
|
||||
|
||||
if(lobbyEl)
|
||||
const centerView = () =>
|
||||
{
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const lobbyRect = lobbyEl.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight - 55;
|
||||
|
||||
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
|
||||
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
|
||||
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
|
||||
|
||||
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
|
||||
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
|
||||
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
|
||||
}
|
||||
if(lobbyEl)
|
||||
{
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const lobbyRect = lobbyEl.getBoundingClientRect();
|
||||
|
||||
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
|
||||
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
|
||||
|
||||
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
|
||||
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
|
||||
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
|
||||
}
|
||||
};
|
||||
|
||||
centerView();
|
||||
|
||||
window.addEventListener('resize', centerView);
|
||||
|
||||
return () => window.removeEventListener('resize', centerView);
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) =>
|
||||
|
||||
@@ -11,8 +11,22 @@ export const InventoryBadgeItemView: FC<PropsWithChildren<{ badgeCode: string }>
|
||||
const { isUnseen = null } = useInventoryUnseenTracker();
|
||||
const unseen = isUnseen(UnseenItemCategory.BADGE, getBadgeId(badgeCode));
|
||||
|
||||
const onDragStart = (event: React.DragEvent<HTMLDivElement>) =>
|
||||
{
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('source', 'inventory');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item itemActive={ (selectedBadgeCode === badgeCode) } itemUnseen={ unseen } onDoubleClick={ event => toggleBadge(selectedBadgeCode) } onMouseDown={ event => setSelectedBadgeCode(badgeCode) } { ...rest }>
|
||||
<InfiniteGrid.Item
|
||||
draggable
|
||||
itemActive={ (selectedBadgeCode === badgeCode) }
|
||||
itemUnseen={ unseen }
|
||||
onDoubleClick={ event => toggleBadge(selectedBadgeCode) }
|
||||
onDragStart={ onDragStart }
|
||||
onMouseDown={ event => setSelectedBadgeCode(badgeCode) }
|
||||
{ ...rest }>
|
||||
<LayoutBadgeImageView badgeCode={ badgeCode } />
|
||||
{ children }
|
||||
</InfiniteGrid.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FaTrashAlt } from 'react-icons/fa';
|
||||
import { LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../common';
|
||||
@@ -7,14 +7,74 @@ import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from '
|
||||
import { InfiniteGrid, NitroButton } from '../../../../layout';
|
||||
import { InventoryBadgeItemView } from './InventoryBadgeItemView';
|
||||
|
||||
const ActiveBadgeSlot: FC<{
|
||||
slotIndex: number;
|
||||
badgeCode?: string;
|
||||
onDropBadge: (badgeCode: string, slotIndex: number, sourceSlot?: number) => void;
|
||||
onRemoveBadge: (badgeCode: string) => void;
|
||||
onDragStartFromSlot: (event: React.DragEvent, badgeCode: string, slotIndex: number) => void;
|
||||
onSelectBadge: (badgeCode: string) => void;
|
||||
isSelected: boolean;
|
||||
}> = ({ slotIndex, badgeCode, onDropBadge, onRemoveBadge, onDragStartFromSlot, onSelectBadge, isSelected }) =>
|
||||
{
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback(() => setIsDragOver(false), []);
|
||||
|
||||
const onDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const sourceSlotStr = event.dataTransfer.getData('activeSlot');
|
||||
const sourceSlot = sourceSlotStr ? parseInt(sourceSlotStr) : undefined;
|
||||
|
||||
if(droppedBadgeCode) onDropBadge(droppedBadgeCode, slotIndex, sourceSlot);
|
||||
}, [ slotIndex, onDropBadge ]);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!badgeCode) return;
|
||||
onDragStartFromSlot(event, badgeCode, slotIndex);
|
||||
}, [ badgeCode, slotIndex, onDragStartFromSlot ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ `flex items-center justify-center rounded-md border-2 cursor-pointer aspect-square transition-colors
|
||||
${ isDragOver ? 'border-blue-400 bg-blue-400/20' : '' }
|
||||
${ isSelected && badgeCode ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ !badgeCode ? 'border-dashed opacity-60' : '' }` }
|
||||
draggable={ !!badgeCode }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
onDrop={ onDrop }
|
||||
onMouseDown={ () => badgeCode && onSelectBadge(badgeCode) }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } />
|
||||
: <span className="text-xs text-white/30">{ slotIndex + 1 }</span> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =>
|
||||
{
|
||||
const { filteredBadgeCodes = null } = props;
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, activate = null, deactivate = null } = useInventoryBadges();
|
||||
const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, activate = null, deactivate = null } = useInventoryBadges();
|
||||
const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker();
|
||||
const { showConfirm = null } = useNotification();
|
||||
const [ isDragOverInventory, setIsDragOverInventory ] = useState(false);
|
||||
|
||||
const maxSlots = 5;
|
||||
const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
||||
|
||||
const attemptDeleteBadge = () =>
|
||||
@@ -31,6 +91,58 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
);
|
||||
};
|
||||
|
||||
const handleDropOnSlot = useCallback((badgeCode: string, slotIndex: number, sourceSlot?: number) =>
|
||||
{
|
||||
if(sourceSlot !== undefined)
|
||||
{
|
||||
// Reorder within active badges
|
||||
reorderBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drop from inventory to active slot
|
||||
setBadgeAtSlot(badgeCode, slotIndex);
|
||||
}
|
||||
}, [ setBadgeAtSlot, reorderBadges ]);
|
||||
|
||||
const handleDragStartFromSlot = useCallback((event: React.DragEvent, badgeCode: string, slotIndex: number) =>
|
||||
{
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('activeSlot', slotIndex.toString());
|
||||
event.dataTransfer.setData('source', 'active');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}, []);
|
||||
|
||||
const handleRemoveBadge = useCallback((badgeCode: string) =>
|
||||
{
|
||||
removeBadge(badgeCode);
|
||||
}, [ removeBadge ]);
|
||||
|
||||
// Handle drop on inventory area (remove from active)
|
||||
const onInventoryDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
const source = event.dataTransfer.types.includes('activeslot') ? 'active' : '';
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOverInventory(true);
|
||||
}, []);
|
||||
|
||||
const onInventoryDragLeave = useCallback(() => setIsDragOverInventory(false), []);
|
||||
|
||||
const onInventoryDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOverInventory(false);
|
||||
|
||||
const badgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const source = event.dataTransfer.getData('source');
|
||||
|
||||
if(source === 'active' && badgeCode)
|
||||
{
|
||||
removeBadge(badgeCode);
|
||||
}
|
||||
}, [ removeBadge ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!selectedBadgeCode || !isUnseen(UnseenItemCategory.BADGE, getBadgeId(selectedBadgeCode))) return;
|
||||
@@ -56,7 +168,11 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="flex flex-col col-span-7 gap-1 overflow-hidden">
|
||||
<div
|
||||
className={ `flex flex-col col-span-7 gap-1 overflow-hidden rounded transition-colors ${ isDragOverInventory ? 'bg-blue-400/10' : '' }` }
|
||||
onDragLeave={ onInventoryDragLeave }
|
||||
onDragOver={ onInventoryDragOver }
|
||||
onDrop={ onInventoryDrop }>
|
||||
<InfiniteGrid<string>
|
||||
columnCount={ 5 }
|
||||
estimateSize={ 50 }
|
||||
@@ -66,11 +182,20 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
<div className="flex flex-col justify-between col-span-5 overflow-auto">
|
||||
<div className="flex flex-col gap-2 overflow-hidden">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">{ LocalizeText('inventory.badges.activebadges') }</span>
|
||||
<InfiniteGrid<string>
|
||||
columnCount={ 3 }
|
||||
estimateSize={ 50 }
|
||||
itemRender={ item => <InventoryBadgeItemView badgeCode={ item } /> }
|
||||
items={ activeBadgeCodes } />
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ Array.from({ length: maxSlots }).map((_, index) => (
|
||||
<ActiveBadgeSlot
|
||||
key={ index }
|
||||
badgeCode={ activeBadgeCodes[index] }
|
||||
isSelected={ selectedBadgeCode === activeBadgeCodes[index] && !!activeBadgeCodes[index] }
|
||||
slotIndex={ index }
|
||||
onDropBadge={ handleDropOnSlot }
|
||||
onDragStartFromSlot={ handleDragStartFromSlot }
|
||||
onRemoveBadge={ handleRemoveBadge }
|
||||
onSelectBadge={ setSelectedBadgeCode }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
{ !!selectedBadgeCode &&
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -117,23 +117,31 @@ export const ModToolsView: FC<{}> = props =>
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ openRoomInfo, closeRoomInfo, toggleRoomInfo, openRoomChatlog, closeRoomChatlog, toggleRoomChatlog, openUserInfo, closeUserInfo, toggleUserInfo, openUserChatlog, closeUserChatlog, toggleUserChatlog ]);
|
||||
|
||||
const isRoomInfoOpen = currentRoomId > 0 && openRooms.includes(currentRoomId);
|
||||
const isRoomChatlogOpen = currentRoomId > 0 && openRoomChatlogs.includes(currentRoomId);
|
||||
const isUserInfoOpen = selectedUser && openUserInfos.includes(selectedUser.userId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isVisible &&
|
||||
<NitroCardView className="nitro-mod-tools" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } >
|
||||
<NitroCardView className="nitro-mod-tools min-w-[200px]" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } >
|
||||
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardContentView className="text-black" gap={ 1 }>
|
||||
<Button className="relative" disabled={ (currentRoomId <= 0) } gap={ 1 } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }>
|
||||
<div className="nitro-icon icon-small-room absolute inset-s-1" /> Room Tool
|
||||
<NitroCardContentView className="text-black" gap={ 2 }>
|
||||
<Button active={ isRoomInfoOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } justifyContent="start" onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }>
|
||||
<div className="nitro-icon icon-small-room shrink-0" /> Room Tool
|
||||
</Button>
|
||||
<Button className="relative" disabled={ (currentRoomId <= 0) } gap={ 1 } innerRef={ elementRef } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }>
|
||||
<div className="nitro-icon icon-chat-history absolute inset-s-1" /> Chatlog Tool
|
||||
<Button active={ isRoomChatlogOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } innerRef={ elementRef } justifyContent="start" onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }>
|
||||
<div className="nitro-icon icon-chat-history shrink-0" /> Chatlog Tool
|
||||
</Button>
|
||||
<Button className="relative" disabled={ !selectedUser } gap={ 1 } onClick={ () => CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }>
|
||||
<div className="nitro-icon icon-user absolute inset-s-1" /> User: { selectedUser ? selectedUser.username : '' }
|
||||
<Button active={ !!isUserInfoOpen } disabled={ !selectedUser } gap={ 2 } justifyContent="start" onClick={ () => CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }>
|
||||
<div className="nitro-icon icon-user shrink-0" />
|
||||
{ selectedUser
|
||||
? <span className="truncate">{ selectedUser.username }</span>
|
||||
: <span className="opacity-50 italic">Select a user</span>
|
||||
}
|
||||
</Button>
|
||||
<Button className="relative" gap={ 1 } onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }>
|
||||
<div className="nitro-icon icon-tickets absolute inset-s-1" /> Report Tool
|
||||
<Button active={ isTicketsVisible } gap={ 2 } justifyContent="start" onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }>
|
||||
<div className="nitro-icon icon-tickets shrink-0" /> Report Tool
|
||||
</Button>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView> }
|
||||
|
||||
@@ -46,14 +46,11 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
|
||||
const RoomInfo = (props: { roomId: number, roomName: string }) =>
|
||||
{
|
||||
return (
|
||||
<Flex alignItems="center" className="bg-muted rounded p-1" gap={ 2 } justifyContent="between">
|
||||
<div className="flex gap-1">
|
||||
<Text bold>Room name:</Text>
|
||||
<Text>{ props.roomName }</Text>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={ event => TryVisitRoom(props.roomId) }>Visit Room</Button>
|
||||
<Button onClick={ event => openRoomInfo(props.roomId) }>Room Tools</Button>
|
||||
<Flex alignItems="center" className="bg-muted rounded p-2" gap={ 2 } justifyContent="between">
|
||||
<Text bold truncate>{ props.roomName }</Text>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<Button size="sm" onClick={ event => TryVisitRoom(props.roomId) }>Visit</Button>
|
||||
<Button size="sm" onClick={ event => openRoomInfo(props.roomId) }>Room Tools</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
@@ -63,7 +60,7 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
|
||||
<>
|
||||
<Column fit gap={ 0 } overflow="hidden">
|
||||
<Column gap={ 2 }>
|
||||
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
|
||||
<Grid className="text-black font-bold border-bottom pb-1 text-[11px] uppercase opacity-60 tracking-wider" gap={ 1 }>
|
||||
<div className="col-span-2">Time</div>
|
||||
<div className="col-span-3">User</div>
|
||||
<div className="col-span-7">Message</div>
|
||||
@@ -77,8 +74,8 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
|
||||
{ row.isRoomInfo &&
|
||||
<RoomInfo roomId={ row.roomId } roomName={ row.roomName } /> }
|
||||
{ !row.isRoomInfo &&
|
||||
<Grid alignItems="center" className="log-entry py-1 border-bottom" fullHeight={ false } gap={ 1 }>
|
||||
<Text className="col-span-2">{ row.timestamp }</Text>
|
||||
<Grid alignItems="center" className="log-entry py-1.5 border-bottom even:bg-black/[0.03]" fullHeight={ false } gap={ 1 }>
|
||||
<Text className="col-span-2 opacity-60 text-[11px]">{ row.timestamp }</Text>
|
||||
<Text bold pointer underline className="col-span-3" onClick={ event => CreateLinkEvent(`mod-tools/open-user-info/${ row.habboId }`) }>{ row.username }</Text>
|
||||
<Text textBreak wrap className="col-span-7">{ row.message }</Text>
|
||||
</Grid> }
|
||||
|
||||
@@ -33,9 +33,9 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
|
||||
if(!roomChatlog) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||
<NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black" overflow="hidden">
|
||||
<NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||
<NitroCardHeaderView headerText={ `Room Chatlog` } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black" overflow="auto">
|
||||
{ roomChatlog &&
|
||||
<ChatlogView records={ [ roomChatlog ] } /> }
|
||||
</NitroCardContentView>
|
||||
|
||||
@@ -69,47 +69,52 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
|
||||
}, [ roomId, infoRequested, setInfoRequested ]);
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-mod-tools-room" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => onCloseClick() } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<NitroCardView className="nitro-mod-tools-room min-w-[280px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||
<NitroCardHeaderView headerText={ 'Room Info' } onCloseClick={ event => onCloseClick() } />
|
||||
<NitroCardContentView className="text-black" gap={ 2 }>
|
||||
{ name &&
|
||||
<div className="bg-muted rounded px-2 py-1.5 text-center">
|
||||
<Text bold truncate>{ name }</Text>
|
||||
</div>
|
||||
}
|
||||
<div className="flex gap-2">
|
||||
<Column grow gap={ 1 } justifyContent="center">
|
||||
<div className="items-center gap-2">
|
||||
<Text bold align="end" className="col-span-7">Room Owner:</Text>
|
||||
<Text pointer truncate underline>{ ownerName }</Text>
|
||||
<Column grow gap={ 1 }>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text bold className="opacity-60 shrink-0">Owner:</Text>
|
||||
<Text bold pointer truncate underline onClick={ () => CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName }</Text>
|
||||
</div>
|
||||
<div className="items-center gap-2">
|
||||
<Text bold align="end" className="col-span-7">Users in room:</Text>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text bold className="opacity-60 shrink-0">Users in room:</Text>
|
||||
<Text>{ usersInRoom }</Text>
|
||||
</div>
|
||||
<div className="items-center gap-2">
|
||||
<Text bold align="end" className="col-span-7">Owner in room:</Text>
|
||||
<Text>{ ownerInRoom ? 'Yes' : 'No' }</Text>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text bold className="opacity-60 shrink-0">Owner here:</Text>
|
||||
<Text className={ ownerInRoom ? 'text-green-700' : 'text-red-700' }>{ ownerInRoom ? 'Yes' : 'No' }</Text>
|
||||
</div>
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1 shrink-0">
|
||||
<Button onClick={ event => TryVisitRoom(roomId) }>Visit Room</Button>
|
||||
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }>Chatlog</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Column className="bg-muted rounded p-2" gap={ 1 }>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input checked={ kickUsers } className="form-check-input" type="checkbox" onChange={ event => setKickUsers(event.target.checked) } />
|
||||
<Text small>Kick everyone out</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input checked={ lockRoom } className="form-check-input" type="checkbox" onChange={ event => setLockRoom(event.target.checked) } />
|
||||
<Text small>Enable the doorbell</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input checked={ changeRoomName } className="form-check-input" type="checkbox" onChange={ event => setChangeRoomName(event.target.checked) } />
|
||||
<Text small>Change room name</Text>
|
||||
</div>
|
||||
</Column>
|
||||
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" placeholder="Type a mandatory message to the users in this text box..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
|
||||
<div className="flex justify-between">
|
||||
<Button variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
|
||||
<Button onClick={ event => handleClick('alert_only') }>Send Alert only</Button>
|
||||
<textarea className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-black/10" placeholder="Type a mandatory message..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
|
||||
<div className="flex gap-2">
|
||||
<Button className="grow" variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
|
||||
<Button className="grow" onClick={ event => handleClick('alert_only') }>Send Alert</Button>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { FC, useRef } from 'react';
|
||||
import { SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Grid } from '../../../../common';
|
||||
|
||||
@@ -12,6 +12,17 @@ interface ModToolsMyIssuesTabViewProps
|
||||
export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =>
|
||||
{
|
||||
const { myIssues = null, handleIssue = null } = props;
|
||||
const pendingReleasesRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const releaseIssue = (issueId: number) =>
|
||||
{
|
||||
if(pendingReleasesRef.current.has(issueId)) return;
|
||||
|
||||
pendingReleasesRef.current.add(issueId);
|
||||
SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ]));
|
||||
|
||||
setTimeout(() => pendingReleasesRef.current.delete(issueId), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap={ 0 } overflow="hidden">
|
||||
@@ -36,7 +47,7 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
|
||||
<Button variant="primary" onClick={ event => handleIssue(issue.issueId) }>Handle</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button variant="danger" onClick={ event => SendMessageComposer(new ReleaseIssuesMessageComposer([ issue.issueId ])) }>Release</Button>
|
||||
<Button variant="danger" onClick={ () => releaseIssue(issue.issueId) }>Release</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { FC, useRef } from 'react';
|
||||
import { SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Grid } from '../../../../common';
|
||||
|
||||
@@ -11,6 +11,17 @@ interface ModToolsOpenIssuesTabViewProps
|
||||
export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = props =>
|
||||
{
|
||||
const { openIssues = null } = props;
|
||||
const pendingPicksRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const pickIssue = (issueId: number) =>
|
||||
{
|
||||
if(pendingPicksRef.current.has(issueId)) return;
|
||||
|
||||
pendingPicksRef.current.add(issueId);
|
||||
SendMessageComposer(new PickIssuesMessageComposer([ issueId ], false, 0, 'pick issue button'));
|
||||
|
||||
setTimeout(() => pendingPicksRef.current.delete(issueId), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap={ 0 } overflow="hidden">
|
||||
@@ -31,7 +42,7 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
|
||||
<div className="col-span-3">{ issue.reportedUserName }</div>
|
||||
<div className="col-span-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
|
||||
<div className="col-span-3">
|
||||
<Button variant="success" onClick={ event => SendMessageComposer(new PickIssuesMessageComposer([ issue.issueId ], false, 0, 'pick issue button')) }>Pick Issue</Button>
|
||||
<Button variant="success" onClick={ () => pickIssue(issue.issueId) }>Pick Issue</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { FC, useMemo, useRef, useState } from 'react';
|
||||
import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api';
|
||||
import { Button, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { useModTools, useNotification } from '../../../../hooks';
|
||||
@@ -33,6 +33,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
|
||||
const [ message, setMessage ] = useState<string>('');
|
||||
const { cfhCategories = null, settings = null } = useModTools();
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const isSendingRef = useRef<boolean>(false);
|
||||
|
||||
const topics = useMemo(() =>
|
||||
{
|
||||
@@ -53,6 +54,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
|
||||
|
||||
const sendDefaultSanction = () =>
|
||||
{
|
||||
if(isSendingRef.current) return;
|
||||
|
||||
let errorMessage: string = null;
|
||||
|
||||
const category = topics[selectedTopic];
|
||||
@@ -63,6 +66,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
|
||||
|
||||
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
|
||||
|
||||
isSendingRef.current = true;
|
||||
|
||||
SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault));
|
||||
|
||||
onCloseClick();
|
||||
@@ -70,6 +75,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
|
||||
|
||||
const sendSanction = () =>
|
||||
{
|
||||
if(isSendingRef.current) return;
|
||||
|
||||
let errorMessage: string = null;
|
||||
|
||||
const category = topics[selectedTopic];
|
||||
@@ -145,6 +152,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
|
||||
}
|
||||
}
|
||||
|
||||
isSendingRef.current = true;
|
||||
|
||||
onCloseClick();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { Button, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '..
|
||||
import { useNavigator } from '../../../hooks';
|
||||
import { NitroInput } from '../../../layout';
|
||||
|
||||
let isCreatingRoom = false;
|
||||
let createRoomTimeout: ReturnType<typeof setTimeout> = null;
|
||||
|
||||
export const NavigatorRoomCreatorView: FC<{}> = props =>
|
||||
{
|
||||
const [ maxVisitorsList, setMaxVisitorsList ] = useState<number[]>(null);
|
||||
@@ -16,6 +19,7 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
|
||||
const [ tradesSetting, setTradesSetting ] = useState<number>(0);
|
||||
const [ roomModels, setRoomModels ] = useState<IRoomModel[]>([]);
|
||||
const [ selectedModelName, setSelectedModelName ] = useState<string>('');
|
||||
const [ isCreating, setIsCreating ] = useState<boolean>(isCreatingRoom);
|
||||
const { categories = null } = useNavigator();
|
||||
|
||||
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
|
||||
@@ -31,7 +35,19 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
|
||||
|
||||
const createRoom = () =>
|
||||
{
|
||||
if(isCreatingRoom) return;
|
||||
|
||||
isCreatingRoom = true;
|
||||
setIsCreating(true);
|
||||
|
||||
SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting));
|
||||
|
||||
if(createRoomTimeout) clearTimeout(createRoomTimeout);
|
||||
createRoomTimeout = setTimeout(() =>
|
||||
{
|
||||
isCreatingRoom = false;
|
||||
setIsCreating(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
@@ -117,7 +133,7 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
|
||||
}
|
||||
</div>
|
||||
</Grid>
|
||||
<Button fullWidth disabled={ (!name || (name.length < 3)) } variant={ (!name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
|
||||
<Button fullWidth disabled={ isCreating || !name || (name.length < 3) } variant={ (isCreating || !name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,6 +162,11 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
|
||||
<input className="form-check-input" type="checkbox" checked={ roomData.allowWalkthrough } onChange={ event => handleChange('allow_walkthrough', event.target.checked) } />
|
||||
<Text>{ LocalizeText('navigator.roomsettings.allow_walk_through') }</Text>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Base className="col-3" />
|
||||
<input className="form-check-input" type="checkbox" checked={ roomData.allowUnderpass } onChange={ event => handleChange('allow_underpass', event.target.checked) } />
|
||||
<Text>{ LocalizeText('navigator.roomsettings.allow_underpass') }</Text>
|
||||
</Flex>
|
||||
<Text variant="danger" underline bold pointer className="d-flex justify-content-center align-items-center gap-1" onClick={ deleteRoom }>
|
||||
<FaTimes className="fa-icon" /> { LocalizeText('navigator.roomsettings.delete') }
|
||||
</Text>
|
||||
|
||||
+15
-4
@@ -1,5 +1,5 @@
|
||||
import { FlatControllerAddedEvent, FlatControllerRemovedEvent, FlatControllersEvent, RemoveAllRightsMessageComposer, RoomGiveRightsComposer, RoomTakeRightsComposer, RoomUsersWithRightsComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
|
||||
import { useFriends, useMessageEvent } from '../../../../hooks';
|
||||
@@ -18,6 +18,17 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
||||
const { roomData = null } = props;
|
||||
const [ usersWithRights, setUsersWithRights ] = useState<Map<number, string>>(new Map());
|
||||
const { onlineFriends = [], offlineFriends = [] } = useFriends();
|
||||
const pendingActionsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const guardedSend = (key: string, composer: any) =>
|
||||
{
|
||||
if(pendingActionsRef.current.has(key)) return;
|
||||
|
||||
pendingActionsRef.current.add(key);
|
||||
SendMessageComposer(composer);
|
||||
|
||||
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
|
||||
};
|
||||
|
||||
const allFriendsRaw = [ ...onlineFriends, ...offlineFriends ];
|
||||
|
||||
@@ -115,7 +126,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
||||
<Text
|
||||
pointer
|
||||
grow
|
||||
onClick={ () => SendMessageComposer(new RoomTakeRightsComposer(id)) }>
|
||||
onClick={ () => guardedSend(`take_${id}`, new RoomTakeRightsComposer(id)) }>
|
||||
{ name }
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -127,7 +138,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
||||
<Button
|
||||
variant="danger"
|
||||
disabled={ !filteredUsersWithRights.size }
|
||||
onClick={ () => roomData && SendMessageComposer(new RemoveAllRightsMessageComposer(roomData.roomId)) }>
|
||||
onClick={ () => roomData && guardedSend('removeAll', new RemoveAllRightsMessageComposer(roomData.roomId)) }>
|
||||
{ LocalizeText('navigator.flatctrls.clear') }
|
||||
</Button>
|
||||
</Column>
|
||||
@@ -154,7 +165,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
||||
<Text
|
||||
pointer
|
||||
grow
|
||||
onClick={ () => SendMessageComposer(new RoomGiveRightsComposer(friend.id)) }>
|
||||
onClick={ () => guardedSend(`give_${friend.id}`, new RoomGiveRightsComposer(friend.id)) }>
|
||||
{ friend.name }
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -39,6 +39,7 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
tags: data.tags,
|
||||
tradeState: data.tradeMode,
|
||||
allowWalkthrough: data.allowWalkThrough,
|
||||
allowUnderpass: data.allowUnderpass,
|
||||
lockState: data.doorMode,
|
||||
password: null,
|
||||
allowPets: data.allowPets,
|
||||
@@ -98,6 +99,9 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
case 'allow_walkthrough':
|
||||
newValue.allowWalkthrough = Boolean(value);
|
||||
break;
|
||||
case 'allow_underpass':
|
||||
newValue.allowUnderpass = Boolean(value);
|
||||
break;
|
||||
case 'allow_pets':
|
||||
newValue.allowPets = Boolean(value);
|
||||
break;
|
||||
@@ -171,7 +175,8 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
newValue.chatSettings.weight,
|
||||
newValue.chatSettings.speed,
|
||||
newValue.chatSettings.distance,
|
||||
newValue.chatSettings.protection
|
||||
newValue.chatSettings.protection,
|
||||
newValue.allowUnderpass
|
||||
));
|
||||
|
||||
return newValue;
|
||||
|
||||
@@ -43,10 +43,11 @@ export const RoomView: FC<{}> = (props) =>
|
||||
<AnimatePresence>
|
||||
{
|
||||
<motion.div
|
||||
className="w-full h-full"
|
||||
initial={ { opacity: 0 }}
|
||||
animate={ { opacity: 1 }}
|
||||
exit={ { opacity: 0 }}>
|
||||
<div ref={ elementRef } className="w-100 h-100">
|
||||
<div ref={ elementRef } className="w-full h-full">
|
||||
{ roomSession instanceof RoomSession &&
|
||||
<>
|
||||
<RoomWidgetsView />
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
interface InfoStandBadgeSlotProps
|
||||
{
|
||||
slotIndex: number;
|
||||
badgeCode?: string;
|
||||
isOwnUser: boolean;
|
||||
}
|
||||
|
||||
const BadgeMiniPicker: FC<{
|
||||
onSelect: (badgeCode: string) => void;
|
||||
onClose: () => void;
|
||||
activeBadgeCodes: string[];
|
||||
}> = ({ onSelect, onClose, activeBadgeCodes }) =>
|
||||
{
|
||||
const { badgeCodes = [], requestBadges = null } = useInventoryBadges();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [ search, setSearch ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(badgeCodes.length === 0) requestBadges();
|
||||
}, []);
|
||||
|
||||
const availableBadges = badgeCodes.filter(code => !activeBadgeCodes.includes(code));
|
||||
const filtered = search.length > 0
|
||||
? availableBadges.filter(code => code.toLowerCase().includes(search.toLowerCase()))
|
||||
: availableBadges;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const handleClickOutside = (event: MouseEvent) =>
|
||||
{
|
||||
if(ref.current && !ref.current.contains(event.target as Node)) onClose();
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [ onClose ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className="absolute right-[calc(100%+8px)] top-0 z-50 bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
|
||||
onClick={ e => e.stopPropagation() }>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full text-xs text-white bg-white/10 border border-white/20 rounded px-2 py-1 mb-2 outline-none focus:border-white/40"
|
||||
placeholder={ LocalizeText('catalog.search') }
|
||||
type="text"
|
||||
value={ search }
|
||||
onChange={ e => setSearch(e.target.value) }
|
||||
/>
|
||||
{ badgeCodes.length === 0
|
||||
? <span className="text-xs text-white/40 text-center py-2 block">{ LocalizeText('generic.loading') }</span>
|
||||
: (
|
||||
<div className="grid grid-cols-4 gap-1 max-h-[160px] overflow-y-auto">
|
||||
{ filtered.slice(0, 40).map(code => (
|
||||
<div
|
||||
key={ code }
|
||||
className="flex items-center justify-center w-[36px] h-[36px] cursor-pointer rounded border border-transparent hover:border-white/40 hover:bg-white/10 transition-all"
|
||||
onClick={ () => onSelect(code) }>
|
||||
<LayoutBadgeImageView badgeCode={ code } />
|
||||
</div>
|
||||
)) }
|
||||
{ filtered.length === 0 && (
|
||||
<span className="text-xs text-white/40 col-span-4 text-center py-2">{ LocalizeText('generic.no_results_found') }</span>
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) =>
|
||||
{
|
||||
const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null } = useInventoryBadges();
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
const [ showPicker, setShowPicker ] = useState(false);
|
||||
|
||||
const hookBadge = activeBadgeCodes.length > 0 ? (activeBadgeCodes[slotIndex] ?? null) : null;
|
||||
const badgeCode = isOwnUser ? (hookBadge ?? badgeCodeFromProps ?? null) : (badgeCodeFromProps ?? null);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!badgeCode || !isOwnUser) return;
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('infostandSlot', slotIndex.toString());
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}, [ badgeCode, slotIndex, isOwnUser ]);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!isOwnUser) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOver(true);
|
||||
}, [ isOwnUser ]);
|
||||
|
||||
const onDragLeave = useCallback(() => setIsDragOver(false), []);
|
||||
|
||||
const onDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
if(!isOwnUser) return;
|
||||
|
||||
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const sourceSlotStr = event.dataTransfer.getData('infostandSlot');
|
||||
|
||||
if(!droppedBadgeCode) return;
|
||||
|
||||
if(sourceSlotStr !== '')
|
||||
{
|
||||
const sourceSlot = parseInt(sourceSlotStr);
|
||||
|
||||
if(sourceSlot !== slotIndex) swapBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBadgeAtSlot(droppedBadgeCode, slotIndex);
|
||||
}
|
||||
}, [ isOwnUser, slotIndex, swapBadges, setBadgeAtSlot ]);
|
||||
|
||||
const handleSlotClick = useCallback(() =>
|
||||
{
|
||||
if(!isOwnUser || badgeCode) return;
|
||||
|
||||
setShowPicker(true);
|
||||
}, [ isOwnUser, badgeCode ]);
|
||||
|
||||
const handlePickerSelect = useCallback((code: string) =>
|
||||
{
|
||||
setBadgeAtSlot(code, slotIndex);
|
||||
setShowPicker(false);
|
||||
}, [ setBadgeAtSlot, slotIndex ]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={ `flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center transition-all duration-150
|
||||
${ isOwnUser && badgeCode ? 'cursor-grab active:cursor-grabbing' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'cursor-pointer' : '' }
|
||||
${ isOwnUser ? 'hover:scale-110 hover:brightness-125 hover:drop-shadow-[0_0_6px_rgba(255,255,255,0.3)]' : '' }
|
||||
${ isDragOver ? 'scale-115 ring-2 ring-blue-400/60 rounded-sm bg-blue-400/15' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'opacity-40 hover:opacity-70 border border-dashed border-white/20 rounded-sm' : '' }` }
|
||||
draggable={ isOwnUser && !!badgeCode }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
onDrop={ onDrop }
|
||||
onClick={ handleSlotClick }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } />
|
||||
: isOwnUser && <FaPlus className="text-white/30 text-[10px]" /> }
|
||||
</div>
|
||||
{ showPicker && (
|
||||
<BadgeMiniPicker
|
||||
activeBadgeCodes={ activeBadgeCodes }
|
||||
onClose={ () => setShowPicker(false) }
|
||||
onSelect={ handlePickerSelect }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||||
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
||||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||||
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
||||
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
|
||||
@@ -158,31 +159,43 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
/>
|
||||
)}
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[0] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[0]} showInfo={true} />}
|
||||
</div>
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[1] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[1]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[2] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[2]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[3] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[3]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[4] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[4]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
{ GetConfigurationValue<boolean>('user.badges.group.slot.enabled', true)
|
||||
? (
|
||||
<>
|
||||
<div className="flex gap-1">
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={5} badgeCode={avatarInfo.badges[5]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
|
||||
@@ -13,11 +13,16 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.image-rendering-pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
*,
|
||||
*:focus,
|
||||
*:hover {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
@@ -17,9 +17,18 @@ const useInventoryBadgesState = () =>
|
||||
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
|
||||
|
||||
const maxBadgeCount = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||
const localChangeRef = useRef(false);
|
||||
const isWearingBadge = (badgeCode: string) => (activeBadgeCodes.indexOf(badgeCode) >= 0);
|
||||
const canWearBadges = () => (activeBadgeCodes.length < maxBadgeCount);
|
||||
|
||||
const sendActiveBadges = (badges: string[]) =>
|
||||
{
|
||||
localChangeRef.current = true;
|
||||
const composer = new SetActivatedBadgesComposer();
|
||||
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
|
||||
SendMessageComposer(composer);
|
||||
};
|
||||
|
||||
const toggleBadge = (badgeCode: string) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
@@ -30,7 +39,7 @@ const useInventoryBadgesState = () =>
|
||||
|
||||
if(index === -1)
|
||||
{
|
||||
if(!canWearBadges()) return prevValue;
|
||||
if(newValue.length >= maxBadgeCount) return prevValue;
|
||||
|
||||
newValue.push(badgeCode);
|
||||
}
|
||||
@@ -39,11 +48,7 @@ const useInventoryBadgesState = () =>
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
|
||||
const composer = new SetActivatedBadgesComposer();
|
||||
|
||||
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(newValue[i] ?? '');
|
||||
|
||||
SendMessageComposer(composer);
|
||||
sendActiveBadges(newValue);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
@@ -77,7 +82,16 @@ const useInventoryBadgesState = () =>
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setActiveBadgeCodes(parser.getActiveBadgeCodes());
|
||||
// Skip overwriting activeBadgeCodes if we recently made a local change
|
||||
if(localChangeRef.current)
|
||||
{
|
||||
localChangeRef.current = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
setActiveBadgeCodes(parser.getActiveBadgeCodes());
|
||||
}
|
||||
|
||||
setBadgeCodes(allBadgeCodes);
|
||||
});
|
||||
|
||||
@@ -141,7 +155,83 @@ const useInventoryBadgesState = () =>
|
||||
setNeedsUpdate(false);
|
||||
}, [ isVisible, needsUpdate ]);
|
||||
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, activate, deactivate };
|
||||
const setBadgeAtSlot = (badgeCode: string, slotIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
// Build a fixed-size array of maxBadgeCount slots
|
||||
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
|
||||
|
||||
// Remove badge if already in another slot
|
||||
const existingIndex = slots.indexOf(badgeCode);
|
||||
if(existingIndex >= 0) slots[existingIndex] = null;
|
||||
|
||||
// Place badge at target slot
|
||||
slots[slotIndex] = badgeCode;
|
||||
|
||||
// Compact: remove nulls, keep order
|
||||
const result = slots.filter(Boolean) as string[];
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const removeBadge = (badgeCode: string) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
const result = prevValue.filter(code => code !== badgeCode);
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const reorderBadges = (fromIndex: number, toIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
if(fromIndex === toIndex) return prevValue;
|
||||
if(fromIndex >= prevValue.length) return prevValue;
|
||||
|
||||
const newValue = [ ...prevValue ];
|
||||
const [ moved ] = newValue.splice(fromIndex, 1);
|
||||
newValue.splice(toIndex, 0, moved);
|
||||
|
||||
sendActiveBadges(newValue);
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const swapBadges = (fromIndex: number, toIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
if(fromIndex === toIndex) return prevValue;
|
||||
|
||||
// Build fixed-size array so swap works even with empty slots
|
||||
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
|
||||
|
||||
// Swap the two slots
|
||||
const temp = slots[fromIndex];
|
||||
slots[fromIndex] = slots[toIndex];
|
||||
slots[toIndex] = temp;
|
||||
|
||||
// Compact: remove nulls, keep order
|
||||
const result = slots.filter(Boolean) as string[];
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const requestBadges = () =>
|
||||
{
|
||||
SendMessageComposer(new RequestBadgesComposer());
|
||||
};
|
||||
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, activate, deactivate };
|
||||
};
|
||||
|
||||
export const useInventoryBadges = () => useBetween(useInventoryBadgesState);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ColorConverter, GetRenderer, GetRoomEngine, GetStage, IRoomSession, NitroAdjustmentFilter, NitroSprite, NitroTexture, RoomBackgroundColorEvent, RoomEngineEvent, RoomEngineObjectEvent, RoomGeometry, RoomId, RoomObjectCategory, RoomObjectHSLColorEnabledEvent, RoomObjectOperationType, RoomSessionEvent, RoomVariableEnum, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, InitializeRoomInstanceRenderingCanvas, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
|
||||
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
|
||||
import { useNitroEvent, useUiEvent } from '../events';
|
||||
|
||||
const useRoomState = () =>
|
||||
@@ -253,15 +253,20 @@ const useRoomState = () =>
|
||||
|
||||
const resize = (event: UIEvent) =>
|
||||
{
|
||||
const width = Math.floor(window.innerWidth);
|
||||
const height = Math.floor(window.innerHeight);
|
||||
const newWidth = Math.floor(window.innerWidth);
|
||||
const newHeight = Math.floor(window.innerHeight);
|
||||
|
||||
renderer.resize(width, height, window.devicePixelRatio);
|
||||
const offsetX = canvas.screenOffsetX - (newWidth - canvas.width) / 2;
|
||||
const offsetY = canvas.screenOffsetY - (newHeight - canvas.height) / 2;
|
||||
|
||||
background.width = width;
|
||||
background.height = height;
|
||||
renderer.resize(newWidth, newHeight, window.devicePixelRatio);
|
||||
|
||||
InitializeRoomInstanceRenderingCanvas(width, height, 1);
|
||||
background.width = newWidth;
|
||||
background.height = newHeight;
|
||||
|
||||
canvas.initialize(newWidth, newHeight);
|
||||
canvas.screenOffsetX = ~~offsetX;
|
||||
canvas.screenOffsetY = ~~offsetY;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 200000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'src/assets/[name].[ext]',
|
||||
assetFileNames: 'src/assets/[name]-[hash].[ext]',
|
||||
manualChunks: id =>
|
||||
{
|
||||
if(id.includes('node_modules'))
|
||||
|
||||
Reference in New Issue
Block a user