Merge branch 'Dev' into feat/wired-fixes-apr08

This commit is contained in:
DuckieTM
2026-04-13 16:58:14 +02:00
committed by GitHub
48 changed files with 1692 additions and 881 deletions
+1 -6
View File
@@ -1,7 +1,7 @@
import { AvatarEditorFigureCategory, AvatarFigureContainer, AvatarFigurePartType, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, IFigurePartSet, IPalette, IPartColor, SetType, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useBetween } from 'use-between';
import { AvatarEditorColorSorter, AvatarEditorPartSorter, AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } from '../../api';
import { AvatarEditorColorSorter, AvatarEditorPartSorter, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } from '../../api';
import { useMessageEvent } from '../events';
import { useFigureData } from './useFigureData';
@@ -244,11 +244,6 @@ const useAvatarEditorState = () =>
setSavedFigures(savedFigures);
});
useEffect(() =>
{
AvatarEditorThumbnailsHelper.clearCache();
}, [ selectedColorParts ]);
useEffect(() =>
{
if(!isVisible) return;
+50 -44
View File
@@ -11,19 +11,31 @@ const useInventoryBadgesState = () =>
const [ needsUpdate, setNeedsUpdate ] = useState(true);
const [ badgeCodes, setBadgeCodes ] = useState<string[]>([]);
const [ badgeIds, setBadgeIds ] = useState<Map<string, number>>(new Map<string, number>());
const [ activeBadgeCodes, setActiveBadgeCodes ] = useState<string[]>([]);
const [ activeBadgeCodes, setActiveBadgeCodes ] = useState<(string | null)[]>([]);
const [ selectedBadgeCode, setSelectedBadgeCode ] = useState<string>(null);
const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility();
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 pendingUpdatesRef = useRef(0);
const isWearingBadge = (badgeCode: string) => activeBadgeCodes.some(code => code === badgeCode);
const canWearBadges = () => (activeBadgeCodes.filter(Boolean).length < maxBadgeCount);
const sendActiveBadges = (badges: string[]) =>
const toFixedSlots = (arr: (string | null)[]): (string | null)[] =>
{
localChangeRef.current = true;
const seen = new Set<string>();
return Array.from({ length: maxBadgeCount }, (_, i) =>
{
const code = arr[i] || null;
if(!code || seen.has(code)) return null;
seen.add(code);
return code;
});
};
const sendActiveBadges = (badges: (string | null)[]) =>
{
pendingUpdatesRef.current++;
const composer = new SetActivatedBadgesComposer();
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
SendMessageComposer(composer);
@@ -33,24 +45,23 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
const newValue = [ ...prevValue ];
const index = newValue.indexOf(badgeCode);
const slots = toFixedSlots(prevValue);
const index = slots.indexOf(badgeCode);
if(index === -1)
{
if(newValue.length >= maxBadgeCount) return prevValue;
const emptySlot = slots.indexOf(null);
if(emptySlot === -1) return prevValue;
newValue.push(badgeCode);
slots[emptySlot] = badgeCode;
}
else
{
newValue.splice(index, 1);
slots[index] = null;
}
sendActiveBadges(newValue);
return newValue;
sendActiveBadges(slots);
return slots;
});
};
@@ -82,14 +93,15 @@ const useInventoryBadgesState = () =>
return newValue;
});
// Skip overwriting activeBadgeCodes if we recently made a local change
if(localChangeRef.current)
// Skip overwriting activeBadgeCodes if we have pending local changes
if(pendingUpdatesRef.current > 0)
{
localChangeRef.current = false;
pendingUpdatesRef.current--;
}
else
{
setActiveBadgeCodes(parser.getActiveBadgeCodes());
const serverBadges = parser.getActiveBadgeCodes();
setActiveBadgeCodes(toFixedSlots(serverBadges));
}
setBadgeCodes(allBadgeCodes);
@@ -159,8 +171,7 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
// Build a fixed-size array of maxBadgeCount slots
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
const slots = toFixedSlots(prevValue);
// Remove badge if already in another slot
const existingIndex = slots.indexOf(badgeCode);
@@ -169,11 +180,8 @@ const useInventoryBadgesState = () =>
// Place badge at target slot
slots[slotIndex] = badgeCode;
// Compact: remove nulls, keep order
const result = slots.filter(Boolean) as string[];
sendActiveBadges(result);
return result;
sendActiveBadges(slots);
return slots;
});
};
@@ -181,10 +189,14 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
const result = prevValue.filter(code => code !== badgeCode);
const slots = toFixedSlots(prevValue);
const index = slots.indexOf(badgeCode);
if(index === -1) return prevValue;
sendActiveBadges(result);
return result;
slots[index] = null;
sendActiveBadges(slots);
return slots;
});
};
@@ -193,14 +205,14 @@ const useInventoryBadgesState = () =>
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);
const slots = toFixedSlots(prevValue);
const temp = slots[fromIndex];
slots[fromIndex] = slots[toIndex];
slots[toIndex] = temp;
sendActiveBadges(newValue);
return newValue;
sendActiveBadges(slots);
return slots;
});
};
@@ -210,19 +222,13 @@ const useInventoryBadgesState = () =>
{
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 slots = toFixedSlots(prevValue);
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;
sendActiveBadges(slots);
return slots;
});
};
+34 -6
View File
@@ -1,4 +1,4 @@
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer';
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, BadgeReceivedEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer';
import { useCallback, useState } from 'react';
import { useBetween } from 'use-between';
import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api';
@@ -14,6 +14,7 @@ const getTimeZeroPadded = (time: number) =>
};
let modDisclaimerTimeout: ReturnType<typeof setTimeout> = null;
const recentBadgeNotifications = new Set<string>();
const useNotificationState = () =>
{
@@ -67,11 +68,11 @@ const useNotificationState = () =>
const showNitroAlert = useCallback(() => simpleAlert(null, NotificationAlertType.NITRO), [ simpleAlert ]);
const showSingleBubble = useCallback((message: string, type: string, imageUrl: string = null, internalLink: string = null) =>
const showSingleBubble = useCallback((message: string, type: string, imageUrl: string = null, internalLink: string = null, senderName: string = '') =>
{
if(bubblesDisabled) return;
const notificationItem = new NotificationBubbleItem(message, type, imageUrl, internalLink);
const notificationItem = new NotificationBubbleItem(message, type, imageUrl, internalLink, senderName);
setBubbleAlerts(prevValue =>
{
@@ -219,12 +220,36 @@ const useNotificationState = () =>
{
const parser = event.getParser();
const text1 = LocalizeText('achievements.levelup.desc');
// Skip if BadgeReceivedEvent already showed a notification for this badge
if(recentBadgeNotifications.has(parser.data.badgeCode)) return;
recentBadgeNotifications.add(parser.data.badgeCode);
setTimeout(() => recentBadgeNotifications.delete(parser.data.badgeCode), 3000);
const badgeName = LocalizeBadgeName(parser.data.badgeCode);
const badgeImage = GetSessionDataManager().getBadgeUrl(parser.data.badgeCode);
const internalLink = 'questengine/achievements/' + parser.data.category;
showSingleBubble((text1 + ' ' + badgeName), NotificationBubbleType.ACHIEVEMENT, badgeImage, internalLink);
showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.data.badgeCode);
});
useMessageEvent<BadgeReceivedEvent>(BadgeReceivedEvent, event =>
{
const parser = event.getParser();
// Skip if AchievementNotificationMessageEvent already showed a notification for this badge
if(recentBadgeNotifications.has(parser.badgeCode)) return;
recentBadgeNotifications.add(parser.badgeCode);
setTimeout(() => recentBadgeNotifications.delete(parser.badgeCode), 3000);
const badgeName = LocalizeBadgeName(parser.badgeCode);
const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode);
// senderName is non-empty only when a staff member awarded the badge
// via the `:badge` command. Empty for achievements, catalog buys,
// wired rewards, poll rewards, etc.
const senderName = parser.senderName || '';
showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode, senderName);
});
useMessageEvent<ClubGiftNotificationEvent>(ClubGiftNotificationEvent, event =>
@@ -345,6 +370,9 @@ const useNotificationState = () =>
{
const parser = event.getParser();
// Skip badge notifications — handled by BadgeReceivedEvent with "Wear" button
if(parser.type === 'badge_received' || parser.type === 'badges' || parser.type.includes('badge')) return;
showNotification(parser.type, parser.parameters);
});
@@ -127,7 +127,7 @@ const useFurnitureYoutubeWidgetState = () =>
onClose();
});
return { objectId, videoId, videoStart, videoEnd, currentVideoState, selectedVideo, playlists, onClose, previous, next, pause, play, selectVideo };
return { objectId, videoId, videoStart, videoEnd, currentVideoState, selectedVideo, playlists, hasControl, onClose, previous, next, pause, play, selectVideo };
};
export const useFurnitureYoutubeWidget = useFurnitureYoutubeWidgetState;
+3 -14
View File
@@ -1,10 +1,7 @@
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
import { useBetween } from 'use-between';
import { LocalizeText } from '../api';
import { useNotification } from './notification';
const YOUTUBE_REGEX = /(?:http:\/\/|https:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?.*v=|shorts\/)?([a-zA-Z0-9_-]{11})/;
const useOnClickChatState = () =>
{
const { showConfirm = null } = useNotification();
@@ -17,19 +14,11 @@ const useOnClickChatState = () =>
event.preventDefault();
const url = event.target.href;
const youtubeMatch = url.match(YOUTUBE_REGEX);
if(youtubeMatch)
showConfirm(LocalizeText('chat.confirm.openurl', [ 'url' ], [ url ]), () =>
{
CreateLinkEvent('youtube-tv/show/' + youtubeMatch[1]);
}
else
{
showConfirm(LocalizeText('chat.confirm.openurl', [ 'url' ], [ url ]), () =>
{
window.open(url, '_blank');
}, null, null, null, LocalizeText('generic.alert.title'), null, 'link');
}
window.open(url, '_blank');
}, null, null, null, LocalizeText('generic.alert.title'), null, 'link');
};
return { onClickChat };