mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
ESLint --fix: auto-fix brace-style, indent, semi, no-trailing-spaces
Run eslint --fix across src/ to clear ~1900 mechanical lint errors surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler upgrade in the React 19 modernization PR. Issues fixed automatically: - brace-style (Allman): try/catch one-liners reformatted to multi-line - indent: tab-vs-space and depth corrections - semi: missing trailing semicolons - no-trailing-spaces No semantic changes. Remaining 701 errors are real-code issues (set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that need manual per-file review. https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This commit is contained in:
@@ -14,8 +14,8 @@ export const RoomView: FC<{}> = (props) =>
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!roomSession) return;
|
||||
|
||||
if(!roomSession) return;
|
||||
|
||||
const canvas = GetRenderer().canvas;
|
||||
|
||||
if(!canvas) return;
|
||||
@@ -109,10 +109,10 @@ export const RoomView: FC<{}> = (props) =>
|
||||
};
|
||||
}, [roomSession]);
|
||||
|
||||
return (
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="w-full h-full"
|
||||
initial={ { opacity: 0 }}
|
||||
animate={ { opacity: 1 }}
|
||||
|
||||
@@ -577,7 +577,10 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
|
||||
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
|
||||
</svg>
|
||||
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
|
||||
<Text small wrap variant="white">Sprite: { (() =>
|
||||
{
|
||||
const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?';
|
||||
})() }</Text>
|
||||
</div>
|
||||
</div> }
|
||||
{ (!avatarInfo.isWallItem && canMove) &&
|
||||
|
||||
@@ -39,23 +39,23 @@ interface InfoStandWidgetPetViewProps {
|
||||
}
|
||||
|
||||
const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={1} justifyContent="between">
|
||||
<Text small wrap variant="white">
|
||||
{name}
|
||||
</Text>
|
||||
<FaTimes
|
||||
className="cursor-pointer fa-icon"
|
||||
onClick={onClose}
|
||||
aria-label={LocalizeText('generic.close')}
|
||||
title={LocalizeText('generic.close')}
|
||||
/>
|
||||
</Flex>
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={1} justifyContent="between">
|
||||
<Text small wrap variant="white">
|
||||
{name}
|
||||
</Text>
|
||||
<FaTimes
|
||||
className="cursor-pointer fa-icon"
|
||||
onClick={onClose}
|
||||
aria-label={LocalizeText('generic.close')}
|
||||
title={LocalizeText('generic.close')}
|
||||
/>
|
||||
</Flex>
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const MonsterplantStats: FC<{
|
||||
@@ -63,281 +63,290 @@ const MonsterplantStats: FC<{
|
||||
remainingGrowTime: number;
|
||||
remainingTimeToLive: number;
|
||||
}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => (
|
||||
<>
|
||||
<Column center gap={1}>
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
<hr className="m-0" />
|
||||
</Column>
|
||||
<div className="flex flex-col gap-2">
|
||||
{!avatarInfo.dead && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
<>
|
||||
<Column center gap={1}>
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
<hr className="m-0" />
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.wellbeing')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.dead || remainingTimeToLive <= 0
|
||||
? '00:00:00'
|
||||
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{
|
||||
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
{!avatarInfo.dead && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.wellbeing')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.dead || remainingTimeToLive <= 0
|
||||
? '00:00:00'
|
||||
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{
|
||||
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
{remainingGrowTime > 0 && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.growth')}
|
||||
</Text>
|
||||
<LayoutCounterTimeView
|
||||
className="top-2 inset-e-2"
|
||||
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
|
||||
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
|
||||
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
|
||||
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
|
||||
</Text>
|
||||
<LayoutRarityLevelView className="top-2 inset-e-2" level={avatarInfo.rarityLevel} />
|
||||
</Column>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</Column>
|
||||
{remainingGrowTime > 0 && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.growth')}
|
||||
</Text>
|
||||
<LayoutCounterTimeView
|
||||
className="top-2 inset-e-2"
|
||||
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
|
||||
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
|
||||
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
|
||||
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
|
||||
</Text>
|
||||
<LayoutRarityLevelView className="top-2 inset-e-2" level={avatarInfo.rarityLevel} />
|
||||
</Column>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Sub-component: Regular Pet Stats
|
||||
const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column fullWidth className="body-image pet p-1" overflow="hidden">
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
</Column>
|
||||
<Column grow gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.happiness')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-info rounded pet-stats"
|
||||
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
|
||||
/>
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column fullWidth className="body-image pet p-1" overflow="hidden">
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
</Column>
|
||||
<Column grow gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.happiness')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-info rounded pet-stats"
|
||||
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.experience')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-purple rounded pet-stats"
|
||||
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.energy')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.experience')}
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-purple rounded pet-stats"
|
||||
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.energy')}
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
|
||||
</Text>
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) => {
|
||||
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
|
||||
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
|
||||
const { roomSession = null } = useRoom();
|
||||
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
||||
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) =>
|
||||
{
|
||||
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
|
||||
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
|
||||
const { roomSession = null } = useRoom();
|
||||
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
||||
|
||||
useEffect(() => {
|
||||
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
|
||||
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
|
||||
}, [avatarInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [avatarInfo]);
|
||||
|
||||
const processButtonAction = useCallback(
|
||||
async (action: string) => {
|
||||
try {
|
||||
let hideMenu = true;
|
||||
if (!action) return;
|
||||
|
||||
switch (action) {
|
||||
case 'respect':
|
||||
await respectPet(avatarInfo.id);
|
||||
if (petRespectRemaining - 1 >= 1) hideMenu = false;
|
||||
break;
|
||||
case 'buyfood':
|
||||
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
|
||||
break;
|
||||
case 'train':
|
||||
roomSession?.requestPetCommands(avatarInfo.id);
|
||||
break;
|
||||
case 'treat':
|
||||
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
|
||||
break;
|
||||
case 'compost':
|
||||
roomSession?.compostPlant(avatarInfo.id);
|
||||
break;
|
||||
case 'pick_up':
|
||||
roomSession?.pickupPet(avatarInfo.id);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hideMenu) onClose();
|
||||
} catch (error) {
|
||||
console.error(`Failed to process action ${action}:`, error);
|
||||
}
|
||||
},
|
||||
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
useEffect(() =>
|
||||
{
|
||||
action: 'buyfood',
|
||||
label: LocalizeText('infostand.button.buyfood'),
|
||||
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
|
||||
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
|
||||
}, [avatarInfo]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
action: 'train',
|
||||
label: LocalizeText('infostand.button.train'),
|
||||
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'treat',
|
||||
label: LocalizeText('infostand.button.pettreat'),
|
||||
condition:
|
||||
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
|
||||
|
||||
const interval = setInterval(() =>
|
||||
{
|
||||
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [avatarInfo]);
|
||||
|
||||
const processButtonAction = useCallback(
|
||||
async (action: string) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
let hideMenu = true;
|
||||
if (!action) return;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case 'respect':
|
||||
await respectPet(avatarInfo.id);
|
||||
if (petRespectRemaining - 1 >= 1) hideMenu = false;
|
||||
break;
|
||||
case 'buyfood':
|
||||
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
|
||||
break;
|
||||
case 'train':
|
||||
roomSession?.requestPetCommands(avatarInfo.id);
|
||||
break;
|
||||
case 'treat':
|
||||
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
|
||||
break;
|
||||
case 'compost':
|
||||
roomSession?.compostPlant(avatarInfo.id);
|
||||
break;
|
||||
case 'pick_up':
|
||||
roomSession?.pickupPet(avatarInfo.id);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hideMenu) onClose();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error(`Failed to process action ${action}:`, error);
|
||||
}
|
||||
},
|
||||
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
action: 'buyfood',
|
||||
label: LocalizeText('infostand.button.buyfood'),
|
||||
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'train',
|
||||
label: LocalizeText('infostand.button.train'),
|
||||
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'treat',
|
||||
label: LocalizeText('infostand.button.pettreat'),
|
||||
condition:
|
||||
!avatarInfo.dead &&
|
||||
avatarInfo.petType === PetType.MONSTERPLANT &&
|
||||
avatarInfo.energy / avatarInfo.maximumEnergy < 0.98,
|
||||
},
|
||||
{
|
||||
action: 'compost',
|
||||
label: LocalizeText('infostand.button.compost'),
|
||||
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'pick_up',
|
||||
label: LocalizeText('inventory.pets.pickup'),
|
||||
condition: avatarInfo.isOwner,
|
||||
},
|
||||
{
|
||||
action: 'respect',
|
||||
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
|
||||
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
];
|
||||
},
|
||||
{
|
||||
action: 'compost',
|
||||
label: LocalizeText('infostand.button.compost'),
|
||||
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'pick_up',
|
||||
label: LocalizeText('inventory.pets.pickup'),
|
||||
condition: avatarInfo.isOwner,
|
||||
},
|
||||
{
|
||||
action: 'respect',
|
||||
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
|
||||
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
];
|
||||
|
||||
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
|
||||
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
|
||||
|
||||
return (
|
||||
<Column alignItems="end" gap={1}>
|
||||
<Column className="nitro-infostand rounded">
|
||||
<Column className="container-fluid content-area" gap={1} overflow="visible">
|
||||
<PetHeader
|
||||
name={avatarInfo.name}
|
||||
petType={avatarInfo.petType}
|
||||
petBreed={avatarInfo.petBreed}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{avatarInfo.petType === PetType.MONSTERPLANT ? (
|
||||
<MonsterplantStats
|
||||
avatarInfo={avatarInfo}
|
||||
remainingGrowTime={remainingGrowTime}
|
||||
remainingTimeToLive={remainingTimeToLive}
|
||||
/>
|
||||
) : (
|
||||
<RegularPetStats avatarInfo={avatarInfo} />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.ownerId} />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Column alignItems="end" gap={1}>
|
||||
<Column className="nitro-infostand rounded">
|
||||
<Column className="container-fluid content-area" gap={1} overflow="visible">
|
||||
<PetHeader
|
||||
name={avatarInfo.name}
|
||||
petType={avatarInfo.petType}
|
||||
petBreed={avatarInfo.petBreed}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{avatarInfo.petType === PetType.MONSTERPLANT ? (
|
||||
<MonsterplantStats
|
||||
avatarInfo={avatarInfo}
|
||||
remainingGrowTime={remainingGrowTime}
|
||||
remainingTimeToLive={remainingTimeToLive}
|
||||
/>
|
||||
) : (
|
||||
<RegularPetStats avatarInfo={avatarInfo} />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.ownerId} />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
<Flex gap={1} justifyContent="end">
|
||||
{buttons.map(
|
||||
(button) =>
|
||||
button.condition && (
|
||||
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
|
||||
{button.label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</Column>
|
||||
</Column>
|
||||
<Flex gap={1} justifyContent="end">
|
||||
{buttons.map(
|
||||
(button) =>
|
||||
button.condition && (
|
||||
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
|
||||
{button.label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
);
|
||||
};
|
||||
@@ -16,286 +16,312 @@ interface InfoStandWidgetUserViewProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => {
|
||||
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
||||
const [motto, setMotto] = useState<string>(null);
|
||||
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
||||
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
||||
const [backgroundId, setBackgroundId] = useState<number>(null);
|
||||
const [standId, setStandId] = useState<number>(null);
|
||||
const [overlayId, setOverlayId] = useState<number>(null);
|
||||
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { roomSession = null } = useRoom();
|
||||
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =>
|
||||
{
|
||||
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
||||
const [motto, setMotto] = useState<string>(null);
|
||||
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
||||
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
||||
const [backgroundId, setBackgroundId] = useState<number>(null);
|
||||
const [standId, setStandId] = useState<number>(null);
|
||||
const [overlayId, setOverlayId] = useState<number>(null);
|
||||
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { roomSession = null } = useRoom();
|
||||
|
||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
||||
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
||||
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
|
||||
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
|
||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
||||
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
||||
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
|
||||
const handleProfileClick = useCallback(() =>
|
||||
{
|
||||
GetUserProfile(avatarInfo.webID);
|
||||
}, [avatarInfo.webID]);
|
||||
|
||||
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
|
||||
const handleEditClick = useCallback((event: React.MouseEvent) =>
|
||||
{
|
||||
event.stopPropagation(); setIsVisible(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const saveMotto = (motto: string) => {
|
||||
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
|
||||
const saveMotto = (motto: string) =>
|
||||
{
|
||||
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
|
||||
|
||||
roomSession.sendMottoMessage(motto);
|
||||
setIsEditingMotto(false);
|
||||
};
|
||||
|
||||
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
|
||||
|
||||
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
saveMotto((event.target as HTMLInputElement).value);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => {
|
||||
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
||||
|
||||
// Deduplicate badges from server
|
||||
const seen = new Set<string>();
|
||||
const dedupedBadges = event.badges.map(code => {
|
||||
if (!code || seen.has(code)) return '';
|
||||
seen.add(code);
|
||||
return code;
|
||||
});
|
||||
|
||||
const oldBadges = avatarInfo.badges.join('');
|
||||
|
||||
if (oldBadges === dedupedBadges.join('')) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.badges = dedupedBadges;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.figure = event.figure;
|
||||
newValue.motto = event.customInfo;
|
||||
newValue.achievementScore = event.activityPoints;
|
||||
newValue.nickIcon = event.nickIcon;
|
||||
newValue.prefixText = event.prefixText;
|
||||
newValue.prefixColor = event.prefixColor;
|
||||
newValue.prefixIcon = event.prefixIcon;
|
||||
newValue.prefixEffect = event.prefixEffect;
|
||||
newValue.displayOrder = event.displayOrder;
|
||||
newValue.backgroundId = event.backgroundId;
|
||||
newValue.standId = event.standId;
|
||||
newValue.overlayId = event.overlayId;
|
||||
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||
|
||||
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
||||
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
|
||||
const parser = event.getParser();
|
||||
|
||||
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
||||
|
||||
setRelationships(parser);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditingMotto(false);
|
||||
setMotto(avatarInfo.motto);
|
||||
setBackgroundId(avatarInfo.backgroundId);
|
||||
setStandId(avatarInfo.standId);
|
||||
setOverlayId(avatarInfo.overlayId);
|
||||
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
||||
|
||||
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
||||
|
||||
return () => {
|
||||
setRelationships(null);
|
||||
roomSession.sendMottoMessage(motto);
|
||||
setIsEditingMotto(false);
|
||||
};
|
||||
}, [avatarInfo]);
|
||||
|
||||
if (!avatarInfo) return null;
|
||||
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
|
||||
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.webID} />
|
||||
<UserIdentityView
|
||||
className="text-[12px]"
|
||||
displayOrder={ avatarInfo.displayOrder }
|
||||
nameClassName="text-white"
|
||||
nickIcon={ avatarInfo.nickIcon }
|
||||
prefixColor={ avatarInfo.prefixColor }
|
||||
prefixEffect={ avatarInfo.prefixEffect }
|
||||
prefixFont={ avatarInfo.prefixFont }
|
||||
prefixIcon={ avatarInfo.prefixIcon }
|
||||
prefixText={ avatarInfo.prefixText }
|
||||
username={ avatarInfo.name } />
|
||||
</div>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column
|
||||
fullWidth
|
||||
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
||||
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
||||
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
||||
</Column>
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Base
|
||||
className="background-edit-icon background-edit-position"
|
||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||
onClick={handleEditClick}
|
||||
aria-label="Edit profile background"
|
||||
/>
|
||||
)}
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
{ (() => {
|
||||
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
||||
const showGroup = maxSlots <= 5;
|
||||
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
const items: React.ReactNode[] = [];
|
||||
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
||||
switch (event.key)
|
||||
{
|
||||
case 'Enter':
|
||||
saveMotto((event.target as HTMLInputElement).value);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if(showGroup) {
|
||||
items.push(
|
||||
<Flex key="group" 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>
|
||||
);
|
||||
} else {
|
||||
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
||||
}
|
||||
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event =>
|
||||
{
|
||||
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
||||
|
||||
const startIdx = showGroup ? 1 : 2;
|
||||
for(let i = startIdx; i < maxSlots; i++) {
|
||||
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
||||
}
|
||||
// Deduplicate badges from server
|
||||
const seen = new Set<string>();
|
||||
const dedupedBadges = event.badges.map(code =>
|
||||
{
|
||||
if (!code || seen.has(code)) return '';
|
||||
seen.add(code);
|
||||
return code;
|
||||
});
|
||||
|
||||
const rows: React.ReactNode[][] = [];
|
||||
for(let i = 0; i < items.length; i += 2) {
|
||||
rows.push(items.slice(i, i + 2));
|
||||
}
|
||||
const oldBadges = avatarInfo.badges.join('');
|
||||
|
||||
return rows.map((row, idx) => (
|
||||
<Flex key={idx} center gap={1}>{row}</Flex>
|
||||
));
|
||||
})() }
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" gap={2}>
|
||||
<FaPencilAlt className="small fa-icon" />
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
{!isEditingMotto && (
|
||||
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
||||
{motto}
|
||||
</Text>
|
||||
if (oldBadges === dedupedBadges.join('')) return;
|
||||
|
||||
setAvatarInfo(prevValue =>
|
||||
{
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.badges = dedupedBadges;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event =>
|
||||
{
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue =>
|
||||
{
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.figure = event.figure;
|
||||
newValue.motto = event.customInfo;
|
||||
newValue.achievementScore = event.activityPoints;
|
||||
newValue.nickIcon = event.nickIcon;
|
||||
newValue.prefixText = event.prefixText;
|
||||
newValue.prefixColor = event.prefixColor;
|
||||
newValue.prefixIcon = event.prefixIcon;
|
||||
newValue.prefixEffect = event.prefixEffect;
|
||||
newValue.displayOrder = event.displayOrder;
|
||||
newValue.backgroundId = event.backgroundId;
|
||||
newValue.standId = event.standId;
|
||||
newValue.overlayId = event.overlayId;
|
||||
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event =>
|
||||
{
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue =>
|
||||
{
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||
|
||||
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
||||
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
||||
|
||||
setRelationships(parser);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsEditingMotto(false);
|
||||
setMotto(avatarInfo.motto);
|
||||
setBackgroundId(avatarInfo.backgroundId);
|
||||
setStandId(avatarInfo.standId);
|
||||
setOverlayId(avatarInfo.overlayId);
|
||||
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
||||
|
||||
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
||||
|
||||
return () =>
|
||||
{
|
||||
setRelationships(null);
|
||||
};
|
||||
}, [avatarInfo]);
|
||||
|
||||
if (!avatarInfo) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
|
||||
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.webID} />
|
||||
<UserIdentityView
|
||||
className="text-[12px]"
|
||||
displayOrder={ avatarInfo.displayOrder }
|
||||
nameClassName="text-white"
|
||||
nickIcon={ avatarInfo.nickIcon }
|
||||
prefixColor={ avatarInfo.prefixColor }
|
||||
prefixEffect={ avatarInfo.prefixEffect }
|
||||
prefixFont={ avatarInfo.prefixFont }
|
||||
prefixIcon={ avatarInfo.prefixIcon }
|
||||
prefixText={ avatarInfo.prefixText }
|
||||
username={ avatarInfo.name } />
|
||||
</div>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column
|
||||
fullWidth
|
||||
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
||||
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
||||
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
||||
</Column>
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Base
|
||||
className="background-edit-icon background-edit-position"
|
||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||
onClick={handleEditClick}
|
||||
aria-label="Edit profile background"
|
||||
/>
|
||||
)}
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
{ (() =>
|
||||
{
|
||||
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
||||
const showGroup = maxSlots <= 5;
|
||||
|
||||
const items: React.ReactNode[] = [];
|
||||
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
||||
|
||||
if(showGroup)
|
||||
{
|
||||
items.push(
|
||||
<Flex key="group" 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>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
||||
}
|
||||
|
||||
const startIdx = showGroup ? 1 : 2;
|
||||
for(let i = startIdx; i < maxSlots; i++)
|
||||
{
|
||||
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
||||
}
|
||||
|
||||
const rows: React.ReactNode[][] = [];
|
||||
for(let i = 0; i < items.length; i += 2)
|
||||
{
|
||||
rows.push(items.slice(i, i + 2));
|
||||
}
|
||||
|
||||
return rows.map((row, idx) => (
|
||||
<Flex key={idx} center gap={1}>{row}</Flex>
|
||||
));
|
||||
})() }
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" gap={2}>
|
||||
<FaPencilAlt className="small fa-icon" />
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
{!isEditingMotto && (
|
||||
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
||||
{motto}
|
||||
</Text>
|
||||
)}
|
||||
{isEditingMotto && (
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
||||
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
||||
type="text"
|
||||
value={motto}
|
||||
onBlur={onMottoBlur}
|
||||
onChange={event => setMotto(event.target.value)}
|
||||
onKeyDown={onMottoKeyDown}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
||||
</Text>
|
||||
{avatarInfo.carryItem > 0 && (
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
||||
</div>
|
||||
{GetConfigurationValue('user.tags.enabled') && (
|
||||
<Column className="mt-1" gap={1}>
|
||||
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
||||
</Column>
|
||||
)}
|
||||
{isEditingMotto && (
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
||||
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
||||
type="text"
|
||||
value={motto}
|
||||
onBlur={onMottoBlur}
|
||||
onChange={event => setMotto(event.target.value)}
|
||||
onKeyDown={onMottoKeyDown}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
||||
</Text>
|
||||
{avatarInfo.carryItem > 0 && (
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
||||
</div>
|
||||
{GetConfigurationValue('user.tags.enabled') && (
|
||||
<Column className="mt-1" gap={1}>
|
||||
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
||||
</Column>
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
</Column>
|
||||
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<div className="backgrounds-view-container">
|
||||
<BackgroundsView
|
||||
setIsVisible={setIsVisible}
|
||||
selectedBackground={backgroundId}
|
||||
setSelectedBackground={setBackgroundId}
|
||||
selectedStand={standId}
|
||||
setSelectedStand={setStandId}
|
||||
selectedOverlay={overlayId}
|
||||
setSelectedOverlay={setOverlayId}
|
||||
selectedCardBackground={cardBackgroundId}
|
||||
setSelectedCardBackground={setCardBackgroundId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<div className="backgrounds-view-container">
|
||||
<BackgroundsView
|
||||
setIsVisible={setIsVisible}
|
||||
selectedBackground={backgroundId}
|
||||
setSelectedBackground={setBackgroundId}
|
||||
selectedStand={standId}
|
||||
setSelectedStand={setStandId}
|
||||
selectedOverlay={overlayId}
|
||||
setSelectedOverlay={setOverlayId}
|
||||
selectedCardBackground={cardBackgroundId}
|
||||
setSelectedCardBackground={setCardBackgroundId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,42 +11,42 @@ interface ChatInputStyleSelectorViewProps
|
||||
|
||||
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
|
||||
{
|
||||
const { chatStyleIds = null, selectChatStyleId = null } = props;
|
||||
const [ selectorVisible, setSelectorVisible ] = useState(false);
|
||||
const { chatStyleIds = null, selectChatStyleId = null } = props;
|
||||
const [ selectorVisible, setSelectorVisible ] = useState(false);
|
||||
|
||||
const selectStyle = (styleId: number) =>
|
||||
{
|
||||
selectChatStyleId(styleId);
|
||||
setSelectorVisible(false);
|
||||
};
|
||||
const selectStyle = (styleId: number) =>
|
||||
{
|
||||
selectChatStyleId(styleId);
|
||||
setSelectorVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root open={selectorVisible} onOpenChange={setSelectorVisible}>
|
||||
<Popover.Trigger asChild>
|
||||
<div className="chatstyles-anchor">
|
||||
<div className="nitro-icon chatstyles-icon" />
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
sideOffset={12}
|
||||
className="max-w-[276px] not-italic font-normal leading-normal text-left no-underline normal-case tracking-normal whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border border-solid border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
|
||||
>
|
||||
<NitroCardContentView className="bg-transparent max-h-[210px]!" overflow="hidden">
|
||||
<Grid columnCount={3} overflow="auto">
|
||||
{chatStyleIds && chatStyleIds.length > 0 && chatStyleIds.map(styleId => (
|
||||
<Flex key={styleId} center pointer className="h-[35px] w-[65px]" onClick={() => selectStyle(styleId)}>
|
||||
<div className="bubble-container relative w-[50px]">
|
||||
<div className={`relative max-w-[65px] min-h-[26px] text-[14px] chat-bubble bubble-${styleId}`} />
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
<Popover.Arrow className="fill-black" width={14} height={7} />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
return (
|
||||
<Popover.Root open={selectorVisible} onOpenChange={setSelectorVisible}>
|
||||
<Popover.Trigger asChild>
|
||||
<div className="chatstyles-anchor">
|
||||
<div className="nitro-icon chatstyles-icon" />
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
sideOffset={12}
|
||||
className="max-w-[276px] not-italic font-normal leading-normal text-left no-underline normal-case tracking-normal whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border border-solid border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
|
||||
>
|
||||
<NitroCardContentView className="bg-transparent max-h-[210px]!" overflow="hidden">
|
||||
<Grid columnCount={3} overflow="auto">
|
||||
{chatStyleIds && chatStyleIds.length > 0 && chatStyleIds.map(styleId => (
|
||||
<Flex key={styleId} center pointer className="h-[35px] w-[65px]" onClick={() => selectStyle(styleId)}>
|
||||
<div className="bubble-container relative w-[50px]">
|
||||
<div className={`relative max-w-[65px] min-h-[26px] text-[14px] chat-bubble bubble-${styleId}`} />
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
<Popover.Arrow className="fill-black" width={14} height={7} />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -284,7 +284,10 @@ export const ChatInputView: FC<{}> = props =>
|
||||
<ChatInputCommandSelectorView
|
||||
commands={ filteredCommands }
|
||||
selectedIndex={ selectedIndex }
|
||||
onSelect={ (cmd) => { setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } }
|
||||
onSelect={ (cmd) =>
|
||||
{
|
||||
setChatValue(':' + cmd.key + ' '); inputRef.current?.focus();
|
||||
} }
|
||||
onHover={ setSelectedIndex }
|
||||
/> }
|
||||
<div className="flex-1 items-center input-sizer">
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
||||
chooserSelectionVisualizer.clearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isChecked = (id: number) => checkedIds.includes(id);
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
||||
setCheckAll(false);
|
||||
chooserSelectionVisualizer.clearAll();
|
||||
setSelectedItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredItems = useMemo(() =>
|
||||
{
|
||||
@@ -179,7 +179,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
||||
alignItems="center"
|
||||
className={ classNames('rounded p-1', selectedItems.some(item => item.id === row.id) && 'bg-muted') }
|
||||
pointer
|
||||
onClick={ () => { toggleItemSelection(row); if(pickallFurni) checkedId(row.id); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
toggleItemSelection(row); if(pickallFurni) checkedId(row.id);
|
||||
} }
|
||||
>
|
||||
{ pickallFurni && (
|
||||
<input
|
||||
@@ -187,7 +190,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
||||
type="checkbox"
|
||||
checked={ isChecked(row.id) }
|
||||
onChange={ () => checkedId(row.id) }
|
||||
onClick={ e => { e.stopPropagation(); toggleItemSelection(row); } }
|
||||
onClick={ e =>
|
||||
{
|
||||
e.stopPropagation(); toggleItemSelection(row);
|
||||
} }
|
||||
/>
|
||||
)}
|
||||
<Text truncate>
|
||||
|
||||
@@ -20,133 +20,146 @@ const FADE_LENGTH = 75;
|
||||
const SPACE_AROUND_EDGES = 10;
|
||||
|
||||
export const ContextMenuView: FC<ContextMenuViewProps> = ({
|
||||
objectId = -1,
|
||||
category = -1,
|
||||
userType = -1,
|
||||
fades = false,
|
||||
onClose,
|
||||
classNames = [],
|
||||
style = {},
|
||||
children = null,
|
||||
collapsable = false,
|
||||
...rest
|
||||
}) => {
|
||||
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
const [isFading, setIsFading] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const stackRef = useRef<FixedSizeStack>(new FixedSizeStack(LOCATION_STACK_SIZE));
|
||||
const maxStackRef = useRef(-1000000);
|
||||
objectId = -1,
|
||||
category = -1,
|
||||
userType = -1,
|
||||
fades = false,
|
||||
onClose,
|
||||
classNames = [],
|
||||
style = {},
|
||||
children = null,
|
||||
collapsable = false,
|
||||
...rest
|
||||
}) =>
|
||||
{
|
||||
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
const [isFading, setIsFading] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const stackRef = useRef<FixedSizeStack>(new FixedSizeStack(LOCATION_STACK_SIZE));
|
||||
const maxStackRef = useRef(-1000000);
|
||||
|
||||
const updatePosition = useCallback(
|
||||
(bounds: NitroRectangle, location: { x: number; y: number }) => {
|
||||
if (!bounds || !location || !elementRef.current) return;
|
||||
const updatePosition = useCallback(
|
||||
(bounds: NitroRectangle, location: { x: number; y: number }) =>
|
||||
{
|
||||
if (!bounds || !location || !elementRef.current) return;
|
||||
|
||||
let offset = -elementRef.current.offsetHeight;
|
||||
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType)) {
|
||||
offset += bounds.height > 50 ? 15 : 0;
|
||||
} else {
|
||||
offset -= 14;
|
||||
}
|
||||
let offset = -elementRef.current.offsetHeight;
|
||||
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType))
|
||||
{
|
||||
offset += bounds.height > 50 ? 15 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
offset -= 14;
|
||||
}
|
||||
|
||||
stackRef.current.addValue(location.y - bounds.top);
|
||||
let maxStack = stackRef.current.getMax();
|
||||
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED) {
|
||||
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
|
||||
}
|
||||
maxStackRef.current = maxStack;
|
||||
stackRef.current.addValue(location.y - bounds.top);
|
||||
let maxStack = stackRef.current.getMax();
|
||||
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED)
|
||||
{
|
||||
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
|
||||
}
|
||||
maxStackRef.current = maxStack;
|
||||
|
||||
const deltaY = location.y - maxStack;
|
||||
let x = Math.round(location.x - elementRef.current.offsetWidth / 2);
|
||||
let y = Math.round(deltaY + offset);
|
||||
const deltaY = location.y - maxStack;
|
||||
let x = Math.round(location.x - elementRef.current.offsetWidth / 2);
|
||||
let y = Math.round(deltaY + offset);
|
||||
|
||||
const stage = GetStage();
|
||||
const maxLeft = stage.width - elementRef.current.offsetWidth - SPACE_AROUND_EDGES;
|
||||
const maxTop = stage.height - elementRef.current.offsetHeight - SPACE_AROUND_EDGES;
|
||||
const stage = GetStage();
|
||||
const maxLeft = stage.width - elementRef.current.offsetWidth - SPACE_AROUND_EDGES;
|
||||
const maxTop = stage.height - elementRef.current.offsetHeight - SPACE_AROUND_EDGES;
|
||||
|
||||
x = Math.max(SPACE_AROUND_EDGES, Math.min(x, maxLeft));
|
||||
y = Math.max(SPACE_AROUND_EDGES, Math.min(y, maxTop));
|
||||
x = Math.max(SPACE_AROUND_EDGES, Math.min(x, maxLeft));
|
||||
y = Math.max(SPACE_AROUND_EDGES, Math.min(y, maxTop));
|
||||
|
||||
setPos({ x, y });
|
||||
},
|
||||
[userType]
|
||||
);
|
||||
setPos({ x, y });
|
||||
},
|
||||
[userType]
|
||||
);
|
||||
|
||||
const getClassNames = useMemo(() => {
|
||||
const classes = [
|
||||
'nitro-context-menu',
|
||||
'p-[2px]!',
|
||||
'bg-[#1c323f]',
|
||||
'border-2',
|
||||
'border-[solid]',
|
||||
'border-[rgba(255,255,255,.5)]',
|
||||
'rounded-[.25rem]',
|
||||
'text-[.7875rem]',
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const classes = [
|
||||
'nitro-context-menu',
|
||||
'p-[2px]!',
|
||||
'bg-[#1c323f]',
|
||||
'border-2',
|
||||
'border-[solid]',
|
||||
'border-[rgba(255,255,255,.5)]',
|
||||
'rounded-[.25rem]',
|
||||
'text-[.7875rem]',
|
||||
'text-white',
|
||||
'z-40',
|
||||
'pointer-events-auto',
|
||||
'absolute',
|
||||
pos.x !== null ? 'visible' : 'invisible',
|
||||
];
|
||||
if (isCollapsed) classes.push('menu-hidden');
|
||||
return [...classes, ...classNames];
|
||||
}, [pos.x, isCollapsed, classNames]);
|
||||
'z-40',
|
||||
'pointer-events-auto',
|
||||
'absolute',
|
||||
pos.x !== null ? 'visible' : 'invisible',
|
||||
];
|
||||
if (isCollapsed) classes.push('menu-hidden');
|
||||
return [...classes, ...classNames];
|
||||
}, [pos.x, isCollapsed, classNames]);
|
||||
|
||||
const getStyle = useMemo(
|
||||
() => ({
|
||||
left: pos.x ?? 0,
|
||||
top: pos.y ?? 0,
|
||||
transition: isFading ? 'opacity 75ms linear' : undefined,
|
||||
opacity,
|
||||
...style,
|
||||
}),
|
||||
[pos, opacity, isFading, style]
|
||||
);
|
||||
const getStyle = useMemo(
|
||||
() => ({
|
||||
left: pos.x ?? 0,
|
||||
top: pos.y ?? 0,
|
||||
transition: isFading ? 'opacity 75ms linear' : undefined,
|
||||
opacity,
|
||||
...style,
|
||||
}),
|
||||
[pos, opacity, isFading, style]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current) return;
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!elementRef.current) return;
|
||||
|
||||
const update = () => {
|
||||
if (!elementRef.current) return;
|
||||
const roomSession = GetRoomSession();
|
||||
const update = () =>
|
||||
{
|
||||
if (!elementRef.current) return;
|
||||
const roomSession = GetRoomSession();
|
||||
|
||||
if (!roomSession) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (!roomSession)
|
||||
{
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = GetRoomObjectBounds(roomSession.roomId, objectId, category);
|
||||
const location = GetRoomObjectScreenLocation(roomSession.roomId, objectId, category);
|
||||
updatePosition(bounds, location);
|
||||
};
|
||||
const bounds = GetRoomObjectBounds(roomSession.roomId, objectId, category);
|
||||
const location = GetRoomObjectScreenLocation(roomSession.roomId, objectId, category);
|
||||
updatePosition(bounds, location);
|
||||
};
|
||||
|
||||
const ticker = GetTicker();
|
||||
ticker.add(update);
|
||||
const ticker = GetTicker();
|
||||
ticker.add(update);
|
||||
|
||||
return () => ticker.remove(update);
|
||||
}, [objectId, category, updatePosition, onClose]);
|
||||
return () => ticker.remove(update);
|
||||
}, [objectId, category, updatePosition, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fades) return;
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!fades) return;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setIsFading(true);
|
||||
setTimeout(onClose, FADE_LENGTH);
|
||||
}, FADE_DELAY);
|
||||
const timeout = setTimeout(() =>
|
||||
{
|
||||
setIsFading(true);
|
||||
setTimeout(onClose, FADE_LENGTH);
|
||||
}, FADE_DELAY);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fades, onClose]);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fades, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFading) return;
|
||||
setOpacity(0);
|
||||
}, [isFading]);
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!isFading) return;
|
||||
setOpacity(0);
|
||||
}, [isFading]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={getClassNames.join(' ')} style={getStyle} {...rest}>
|
||||
{!(collapsable && isCollapsed) && children}
|
||||
{collapsable && <ContextMenuCaretView collapsed={isCollapsed} onClick={() => setIsCollapsed((prev) => !prev)} />}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div ref={elementRef} className={getClassNames.join(' ')} style={getStyle} {...rest}>
|
||||
{!(collapsable && isCollapsed) && children}
|
||||
{collapsable && <ContextMenuCaretView collapsed={isCollapsed} onClick={() => setIsCollapsed((prev) => !prev)} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,16 +4,19 @@ import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../
|
||||
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
|
||||
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
|
||||
|
||||
export const FurnitureExternalImageView: FC<{}> = props => {
|
||||
export const FurnitureExternalImageView: FC<{}> = props =>
|
||||
{
|
||||
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
|
||||
const { report = null } = useHelp();
|
||||
|
||||
if (objectId === -1 || currentPhotoIndex === -1) return null;
|
||||
|
||||
const handleOpenFullPhoto = () => {
|
||||
const handleOpenFullPhoto = () =>
|
||||
{
|
||||
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
|
||||
if (photoUrl) {
|
||||
console.log("Opened photo URL:", photoUrl);
|
||||
if (photoUrl)
|
||||
{
|
||||
console.log('Opened photo URL:', photoUrl);
|
||||
window.open(photoUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Text } from '../../../../common';
|
||||
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
|
||||
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
|
||||
|
||||
export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
export const RoomToolsWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
|
||||
const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false);
|
||||
const [roomName, setRoomName] = useState<string>(null);
|
||||
@@ -27,19 +28,25 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
return subscribePlugins(() => setPlugins(getRegisteredPlugins()));
|
||||
}, []);
|
||||
|
||||
const handleToolClick = (action: string, value?: string) => {
|
||||
if (!roomSession) return;
|
||||
const handleToolClick = (action: string, value?: string) =>
|
||||
{
|
||||
if (!roomSession) return;
|
||||
|
||||
switch (action) {
|
||||
switch (action)
|
||||
{
|
||||
case 'settings':
|
||||
CreateLinkEvent('navigator/toggle-room-info');
|
||||
return;
|
||||
case 'zoom':
|
||||
setIsZoomedIn(prevValue => {
|
||||
if (GetConfigurationValue('room.zoom.enabled', true)) {
|
||||
setIsZoomedIn(prevValue =>
|
||||
{
|
||||
if (GetConfigurationValue('room.zoom.enabled', true))
|
||||
{
|
||||
const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1);
|
||||
GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1);
|
||||
if (geometry) geometry.performZoom();
|
||||
}
|
||||
@@ -77,7 +84,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeRoomHistory = (roomId: number, roomName: string) => {
|
||||
const onChangeRoomHistory = (roomId: number, roomName: string) =>
|
||||
{
|
||||
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
|
||||
if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
|
||||
|
||||
@@ -88,7 +96,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
SetLocalStorage('nitro.room.history', newStorage);
|
||||
};
|
||||
|
||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event => {
|
||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
|
||||
|
||||
@@ -98,18 +107,22 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsOpen(true);
|
||||
const timeout = setTimeout(() => setIsOpen(false), 5000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [roomName, roomOwner, roomTags]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() =>
|
||||
{
|
||||
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTabClose = () => {
|
||||
useEffect(() =>
|
||||
{
|
||||
const handleTabClose = () =>
|
||||
{
|
||||
window.localStorage.removeItem('nitro.room.history');
|
||||
};
|
||||
window.addEventListener('beforeunload', handleTabClose);
|
||||
@@ -119,10 +132,10 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
||||
return (
|
||||
<div className="flex space-x-2 nitro-room-tools-container">
|
||||
<div className="flex flex-col items-center justify-center p-2 nitro-room-tools">
|
||||
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
|
||||
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
|
||||
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
|
||||
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
|
||||
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
|
||||
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
|
||||
|
||||
{navigatorData.canRate && (
|
||||
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
|
||||
|
||||
Reference in New Issue
Block a user