mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
Merge remote-tracking branch 'duckie-temp/main' into duckie-merge-2026-04-21
# Conflicts: # src/components/room/widgets/chat-input/ChatInputView.tsx # src/components/toolbar/ToolbarView.tsx # src/css/chat/Chats.css # src/css/nitrocard/NitroCardView.css # src/css/purse/PurseView.css # src/css/room/RoomWidgets.css
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user