mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
71171dc205
# Conflicts: # index.html # public/UITexts.example # public/renderer-config.example # src/App.tsx # src/components/login/LoginView.tsx # src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx # src/components/toolbar/ToolbarView.tsx # src/components/user-profile/UserContainerView.tsx
302 lines
14 KiB
TypeScript
302 lines
14 KiB
TypeScript
import React from 'react';
|
||
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
|
||
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, UserIdentityView, UserProfileIconView } from '../../../../../common';
|
||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
||
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
|
||
|
||
interface InfoStandWidgetUserViewProps {
|
||
avatarInfo: AvatarInfoUser;
|
||
setAvatarInfo: Dispatch<SetStateAction<AvatarInfoUser>>;
|
||
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();
|
||
|
||
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 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);
|
||
};
|
||
}, [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>
|
||
)}
|
||
</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>
|
||
)}
|
||
</>
|
||
);
|
||
};
|