mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Merge pull request #78 from simoleo89/badge-dnd-rework
Badge System Rework: DnD fixes, visual feedback, dynamic slots
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { FC, PropsWithChildren } from 'react';
|
||||
import { UnseenItemCategory } from '../../../../api';
|
||||
import { FC, PropsWithChildren, useState } from 'react';
|
||||
import { GetConfigurationValue, UnseenItemCategory } from '../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../common';
|
||||
import { useInventoryBadges, useInventoryUnseenTracker } from '../../../../hooks';
|
||||
import { InfiniteGrid } from '../../../../layout';
|
||||
@@ -10,20 +10,31 @@ export const InventoryBadgeItemView: FC<PropsWithChildren<{ badgeCode: string }>
|
||||
const { selectedBadgeCode = null, setSelectedBadgeCode = null, toggleBadge = null, getBadgeId = null } = useInventoryBadges();
|
||||
const { isUnseen = null } = useInventoryUnseenTracker();
|
||||
const unseen = isUnseen(UnseenItemCategory.BADGE, getBadgeId(badgeCode));
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
|
||||
const onDragStart = (event: React.DragEvent<HTMLDivElement>) =>
|
||||
{
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('source', 'inventory');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
setIsDragging(true);
|
||||
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode);
|
||||
const img = new Image();
|
||||
img.src = badgeUrl;
|
||||
event.dataTransfer.setDragImage(img, 20, 20);
|
||||
};
|
||||
|
||||
const onDragEnd = () => setIsDragging(false);
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item
|
||||
draggable
|
||||
className={ `cursor-grab active:cursor-grabbing ${ isDragging ? 'opacity-40 scale-95' : '' }` }
|
||||
itemActive={ (selectedBadgeCode === badgeCode) }
|
||||
itemUnseen={ unseen }
|
||||
onDoubleClick={ event => toggleBadge(selectedBadgeCode) }
|
||||
onDragEnd={ onDragEnd }
|
||||
onDragStart={ onDragStart }
|
||||
onMouseDown={ event => setSelectedBadgeCode(badgeCode) }
|
||||
{ ...rest }>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaTrashAlt } from 'react-icons/fa';
|
||||
import { LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api';
|
||||
import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../common';
|
||||
import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from '../../../../hooks';
|
||||
import { InfiniteGrid, NitroButton } from '../../../../layout';
|
||||
@@ -18,6 +18,8 @@ const ActiveBadgeSlot: FC<{
|
||||
}> = ({ slotIndex, badgeCode, onDropBadge, onRemoveBadge, onDragStartFromSlot, onSelectBadge, isSelected }) =>
|
||||
{
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [ justDropped, setJustDropped ] = useState(false);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
@@ -35,24 +37,36 @@ const ActiveBadgeSlot: FC<{
|
||||
|
||||
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const sourceSlotStr = event.dataTransfer.getData('activeSlot');
|
||||
const sourceSlot = sourceSlotStr ? parseInt(sourceSlotStr) : undefined;
|
||||
const sourceSlot = sourceSlotStr !== '' ? parseInt(sourceSlotStr) : undefined;
|
||||
|
||||
if(droppedBadgeCode) onDropBadge(droppedBadgeCode, slotIndex, sourceSlot);
|
||||
if(droppedBadgeCode)
|
||||
{
|
||||
onDropBadge(droppedBadgeCode, slotIndex, sourceSlot);
|
||||
setJustDropped(true);
|
||||
setTimeout(() => setJustDropped(false), 300);
|
||||
}
|
||||
}, [ slotIndex, onDropBadge ]);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!badgeCode) return;
|
||||
onDragStartFromSlot(event, badgeCode, slotIndex);
|
||||
setIsDragging(true);
|
||||
}, [ badgeCode, slotIndex, onDragStartFromSlot ]);
|
||||
|
||||
const onDragEnd = useCallback(() => setIsDragging(false), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ `flex items-center justify-center rounded-md border-2 cursor-pointer aspect-square transition-colors
|
||||
${ isDragOver ? 'border-blue-400 bg-blue-400/20' : '' }
|
||||
className={ `flex items-center justify-center rounded-md border-2 aspect-square transition-all duration-150
|
||||
${ badgeCode ? 'cursor-grab active:cursor-grabbing' : 'cursor-default' }
|
||||
${ isDragging ? 'opacity-30 scale-95' : '' }
|
||||
${ isDragOver ? 'border-blue-400 bg-blue-400/20 animate-pulse-glow scale-105' : '' }
|
||||
${ justDropped ? 'animate-drop-settle' : '' }
|
||||
${ isSelected && badgeCode ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ !badgeCode ? 'border-dashed opacity-60' : '' }` }
|
||||
draggable={ !!badgeCode }
|
||||
onDragEnd={ onDragEnd }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
@@ -73,8 +87,9 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker();
|
||||
const { showConfirm = null } = useNotification();
|
||||
const [ isDragOverInventory, setIsDragOverInventory ] = useState(false);
|
||||
const [ isDraggingFromActive, setIsDraggingFromActive ] = useState(false);
|
||||
|
||||
const maxSlots = 5;
|
||||
const maxSlots = useMemo(() => GetConfigurationValue<number>('user.badges.max.slots', 5), []);
|
||||
const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
||||
|
||||
const attemptDeleteBadge = () =>
|
||||
@@ -95,12 +110,10 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
{
|
||||
if(sourceSlot !== undefined)
|
||||
{
|
||||
// Reorder within active badges
|
||||
reorderBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drop from inventory to active slot
|
||||
setBadgeAtSlot(badgeCode, slotIndex);
|
||||
}
|
||||
}, [ setBadgeAtSlot, reorderBadges ]);
|
||||
@@ -111,6 +124,11 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
event.dataTransfer.setData('activeSlot', slotIndex.toString());
|
||||
event.dataTransfer.setData('source', 'active');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode);
|
||||
const img = new Image();
|
||||
img.src = badgeUrl;
|
||||
event.dataTransfer.setDragImage(img, 20, 20);
|
||||
}, []);
|
||||
|
||||
const handleRemoveBadge = useCallback((badgeCode: string) =>
|
||||
@@ -121,18 +139,24 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
// Handle drop on inventory area (remove from active)
|
||||
const onInventoryDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
const source = event.dataTransfer.types.includes('activeslot') ? 'active' : '';
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
const fromActive = event.dataTransfer.types.includes('activeslot');
|
||||
setIsDraggingFromActive(fromActive);
|
||||
setIsDragOverInventory(true);
|
||||
}, []);
|
||||
|
||||
const onInventoryDragLeave = useCallback(() => setIsDragOverInventory(false), []);
|
||||
const onInventoryDragLeave = useCallback(() =>
|
||||
{
|
||||
setIsDragOverInventory(false);
|
||||
setIsDraggingFromActive(false);
|
||||
}, []);
|
||||
|
||||
const onInventoryDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOverInventory(false);
|
||||
setIsDraggingFromActive(false);
|
||||
|
||||
const badgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const source = event.dataTransfer.getData('source');
|
||||
@@ -169,10 +193,18 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
return (
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div
|
||||
className={ `flex flex-col col-span-7 gap-1 overflow-hidden rounded transition-colors ${ isDragOverInventory ? 'bg-blue-400/10' : '' }` }
|
||||
className={ `relative flex flex-col col-span-7 gap-1 overflow-hidden rounded transition-all duration-200
|
||||
${ isDragOverInventory && isDraggingFromActive ? 'bg-red-500/10 ring-2 ring-inset ring-red-400/30 animate-pulse-glow-red' : '' }
|
||||
${ isDragOverInventory && !isDraggingFromActive ? 'bg-blue-400/10' : '' }` }
|
||||
onDragLeave={ onInventoryDragLeave }
|
||||
onDragOver={ onInventoryDragOver }
|
||||
onDrop={ onInventoryDrop }>
|
||||
{ isDragOverInventory && isDraggingFromActive && (
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center pointer-events-none">
|
||||
<FaTrashAlt className="text-red-400/60 text-2xl mb-1" />
|
||||
<span className="text-red-400/60 text-xs font-medium">{ LocalizeText('inventory.badges.clearbadge') }</span>
|
||||
</div>
|
||||
) }
|
||||
<InfiniteGrid<string>
|
||||
columnCount={ 5 }
|
||||
estimateSize={ 50 }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NotificationBubbleItem, NotificationBubbleType } from '../../../../api';
|
||||
import { NotificationBadgeReceivedBubbleView } from './NotificationBadgeReceivedBubbleView';
|
||||
import { NotificationClubGiftBubbleView } from './NotificationClubGiftBubbleView';
|
||||
import { NotificationDefaultBubbleView } from './NotificationDefaultBubbleView';
|
||||
|
||||
@@ -10,6 +11,8 @@ export const GetBubbleLayout = (item: NotificationBubbleItem, onClose: () => voi
|
||||
|
||||
switch(item.notificationType)
|
||||
{
|
||||
case NotificationBubbleType.BADGE_RECEIVED:
|
||||
return <NotificationBadgeReceivedBubbleView key={ item.id } { ...props } />;
|
||||
case NotificationBubbleType.CLUBGIFT:
|
||||
return <NotificationClubGiftBubbleView key={ item.id } { ...props } />;
|
||||
default:
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { RequestBadgesComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { LocalizeText, NotificationBubbleItem, SendMessageComposer } from '../../../../api';
|
||||
import { Flex, LayoutNotificationBubbleView, LayoutNotificationBubbleViewProps, Text } from '../../../../common';
|
||||
import { useInventoryBadges } from '../../../../hooks';
|
||||
|
||||
export interface NotificationBadgeReceivedBubbleViewProps extends LayoutNotificationBubbleViewProps
|
||||
{
|
||||
item: NotificationBubbleItem;
|
||||
}
|
||||
|
||||
export const NotificationBadgeReceivedBubbleView: FC<NotificationBadgeReceivedBubbleViewProps> = props =>
|
||||
{
|
||||
const { item = null, onClose = null, ...rest } = props;
|
||||
const { badgeCodes = [], toggleBadge = null } = useInventoryBadges();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(badgeCodes.length === 0) SendMessageComposer(new RequestBadgesComposer());
|
||||
}, []);
|
||||
|
||||
const handleWear = (event: React.MouseEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
if(item.linkUrl)
|
||||
{
|
||||
toggleBadge(item.linkUrl);
|
||||
}
|
||||
|
||||
if(onClose) onClose();
|
||||
};
|
||||
|
||||
const handleDismiss = (event: React.MouseEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
if(onClose) onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutNotificationBubbleView className="flex-col" onClose={ onClose } { ...rest }>
|
||||
<div onClick={ (e) => e.stopPropagation() }>
|
||||
<Flex alignItems="center" gap={ 2 } className="mb-2">
|
||||
<Flex center className="w-[50px] h-[50px] shrink-0">
|
||||
{ item.iconUrl && <img alt="" className="no-select" src={ item.iconUrl } /> }
|
||||
</Flex>
|
||||
<Flex column gap={ 0 }>
|
||||
<Text bold variant="white">{ LocalizeText('notification.badge.received') }</Text>
|
||||
<Text variant="white" small>{ item.message }</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="end" gap={ 2 }>
|
||||
<button
|
||||
className="btn btn-success w-full btn-sm"
|
||||
type="button"
|
||||
onClick={ handleWear }>
|
||||
{ LocalizeText('inventory.badges.wearbadge') }
|
||||
</button>
|
||||
<span className="underline cursor-pointer text-nowrap" onClick={ handleDismiss }>
|
||||
{ LocalizeText('notifications.button.later') }
|
||||
</span>
|
||||
</Flex>
|
||||
</div>
|
||||
</LayoutNotificationBubbleView>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { GetConfigurationValue, LocalizeText } from '../../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
@@ -14,7 +14,7 @@ interface InfoStandBadgeSlotProps
|
||||
const BadgeMiniPicker: FC<{
|
||||
onSelect: (badgeCode: string) => void;
|
||||
onClose: () => void;
|
||||
activeBadgeCodes: string[];
|
||||
activeBadgeCodes: (string | null)[];
|
||||
}> = ({ onSelect, onClose, activeBadgeCodes }) =>
|
||||
{
|
||||
const { badgeCodes = [], requestBadges = null } = useInventoryBadges();
|
||||
@@ -26,7 +26,8 @@ const BadgeMiniPicker: FC<{
|
||||
if(badgeCodes.length === 0) requestBadges();
|
||||
}, []);
|
||||
|
||||
const availableBadges = badgeCodes.filter(code => !activeBadgeCodes.includes(code));
|
||||
const activeSet = new Set(activeBadgeCodes.filter(Boolean));
|
||||
const availableBadges = badgeCodes.filter(code => !activeSet.has(code));
|
||||
const filtered = search.length > 0
|
||||
? availableBadges.filter(code => code.toLowerCase().includes(search.toLowerCase()))
|
||||
: availableBadges;
|
||||
@@ -78,12 +79,24 @@ const BadgeMiniPicker: FC<{
|
||||
|
||||
export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) =>
|
||||
{
|
||||
const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null } = useInventoryBadges();
|
||||
const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null, removeBadge = null, requestBadges = null } = useInventoryBadges();
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [ justDropped, setJustDropped ] = useState(false);
|
||||
const [ showPicker, setShowPicker ] = useState(false);
|
||||
|
||||
const hookBadge = activeBadgeCodes.length > 0 ? (activeBadgeCodes[slotIndex] ?? null) : null;
|
||||
const badgeCode = isOwnUser ? (hookBadge ?? badgeCodeFromProps ?? null) : (badgeCodeFromProps ?? null);
|
||||
const hookInitialized = activeBadgeCodes.length > 0;
|
||||
|
||||
// Load badge data for own user so hook is initialized before any DnD
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isOwnUser && !hookInitialized) requestBadges();
|
||||
}, [ isOwnUser, hookInitialized, requestBadges ]);
|
||||
const hookBadge = hookInitialized ? (activeBadgeCodes[slotIndex] ?? null) : null;
|
||||
// Once hook has data, use ONLY hook data for own user (no stale props fallback)
|
||||
const badgeCode = isOwnUser
|
||||
? (hookInitialized ? hookBadge : (badgeCodeFromProps ?? null))
|
||||
: (badgeCodeFromProps ?? null);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
@@ -91,8 +104,16 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('infostandSlot', slotIndex.toString());
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
setIsDragging(true);
|
||||
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode);
|
||||
const img = new Image();
|
||||
img.src = badgeUrl;
|
||||
event.dataTransfer.setDragImage(img, 20, 20);
|
||||
}, [ badgeCode, slotIndex, isOwnUser ]);
|
||||
|
||||
const onDragEnd = useCallback(() => setIsDragging(false), []);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!isOwnUser) return;
|
||||
@@ -124,6 +145,9 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
{
|
||||
setBadgeAtSlot(droppedBadgeCode, slotIndex);
|
||||
}
|
||||
|
||||
setJustDropped(true);
|
||||
setTimeout(() => setJustDropped(false), 300);
|
||||
}, [ isOwnUser, slotIndex, swapBadges, setBadgeAtSlot ]);
|
||||
|
||||
const handleSlotClick = useCallback(() =>
|
||||
@@ -133,6 +157,13 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
setShowPicker(true);
|
||||
}, [ isOwnUser, badgeCode ]);
|
||||
|
||||
const handleDoubleClick = useCallback(() =>
|
||||
{
|
||||
if(!isOwnUser || !badgeCode) return;
|
||||
|
||||
removeBadge(badgeCode);
|
||||
}, [ isOwnUser, badgeCode, removeBadge ]);
|
||||
|
||||
const handlePickerSelect = useCallback((code: string) =>
|
||||
{
|
||||
setBadgeAtSlot(code, slotIndex);
|
||||
@@ -145,15 +176,19 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
className={ `flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center transition-all duration-150
|
||||
${ isOwnUser && badgeCode ? 'cursor-grab active:cursor-grabbing' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'cursor-pointer' : '' }
|
||||
${ isOwnUser ? 'hover:scale-110 hover:brightness-125 hover:drop-shadow-[0_0_6px_rgba(255,255,255,0.3)]' : '' }
|
||||
${ isDragOver ? 'scale-115 ring-2 ring-blue-400/60 rounded-sm bg-blue-400/15' : '' }
|
||||
${ isDragging ? 'opacity-30 scale-90' : '' }
|
||||
${ isOwnUser && !isDragging ? 'hover:scale-110 hover:brightness-125 hover:drop-shadow-[0_0_6px_rgba(255,255,255,0.3)]' : '' }
|
||||
${ isDragOver ? 'scale-110 ring-2 ring-blue-400/60 rounded-sm bg-blue-400/15 animate-pulse-glow' : '' }
|
||||
${ justDropped ? 'animate-drop-settle' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'opacity-40 hover:opacity-70 border border-dashed border-white/20 rounded-sm' : '' }` }
|
||||
draggable={ isOwnUser && !!badgeCode }
|
||||
onDragEnd={ onDragEnd }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
onDrop={ onDrop }
|
||||
onClick={ handleSlotClick }>
|
||||
onClick={ handleSlotClick }
|
||||
onDoubleClick={ handleDoubleClick }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } />
|
||||
: isOwnUser && <FaPlus className="text-white/30 text-[10px]" /> }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
@@ -56,15 +57,23 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
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 === event.badges.join('')) return;
|
||||
if (oldBadges === dedupedBadges.join('')) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
if (!prevValue) return prevValue;
|
||||
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.badges = event.badges;
|
||||
newValue.badges = dedupedBadges;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
@@ -165,43 +174,38 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
/>
|
||||
)}
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
{ GetConfigurationValue<boolean>('user.badges.group.slot.enabled', true)
|
||||
? (
|
||||
<>
|
||||
<div className="flex gap-1">
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
{ (() => {
|
||||
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>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={5} badgeCode={avatarInfo.badges[5]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
} 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" />
|
||||
|
||||
Reference in New Issue
Block a user