5180 fichiers 38.5 Mo 0 téléchargements 604 vues mis à jour il y a 12 heures ui
nitro-v36-with-renderer-v36 / src / components / room / widgets / avatar-info / infostand / InfoStandWidgetUserView.tsx
InfoStandWidgetUserView.tsx 19.6 Ko Télécharger le fichier
import {
    CreateLinkEvent,
    GetSessionDataManager,
    RelationshipStatusInfoEvent,
    RelationshipStatusInfoMessageParser,
    RoomSessionFavoriteGroupUpdateEvent,
    RoomSessionUserBadgesEvent,
    RoomSessionUserFigureUpdateEvent,
    UserRelationshipsComposer
} from '@octane/renderer';
import React, { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
import {
    AvatarInfoUser,
    CloneObject,
    ensureBadgeLeaderboardLoaded,
    GetConfigurationValue,
    getBadgesRank,
    GetGroupInformation,
    GetUserProfile,
    LocalizeText,
    localizeWithFallback,
    SendMessageComposer
} from '../../../../../api';
import homeIcon from '../../../../../assets/images/infostand/home-icon.png';
import pencilIcon from '../../../../../assets/images/infostand/pencil-icon.png';
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView } from '../../../../../common';
import { useIsUserBlocked, useMessageEvent, useOctaneEvent, useRoom } from '../../../../../hooks';
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
import { BaseButton } from '../../../../../common/BaseButton';
import { Hint } from '../../../../../common/Tooltip';
import { NativeTextTree } from '../../../../../common/native-text/NativeTextTree';

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 isBlocked = useIsUserBlocked(avatarInfo?.webID ?? 0);
    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 [borderId, setBorderId] = useState<number>(null);
    const [isVisible, setIsVisible] = useState(false);
    const [badgesRank, setBadgesRank] = useState<{ userId: number; rank: number }>(null);
    const { roomSession = null } = useRoom();
    const webID = avatarInfo?.webID ?? 0;
    const packetBadgesRank = avatarInfo?.badgesRank ?? -1;

    // Official badges rank line: the room user list carries it; older servers fall back to the leaderboard.
    useEffect(() => {
        if (webID <= 0 || packetBadgesRank > 0) return;

        let cancelled = false;

        ensureBadgeLeaderboardLoaded()
            .then((leaderboard) => {
                if (!cancelled) setBadgesRank({ userId: webID, rank: getBadgesRank(leaderboard, webID) });
            })
            .catch(() => {});

        return () => {
            cancelled = true;
        };
    }, [webID, packetBadgesRank]);

    const shownBadgesRank = packetBadgesRank > 0 ? packetBadgesRank : badgesRank && badgesRank.userId === webID ? badgesRank.rank : -1;

    const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
    const infostandStandClass = `stand-${standId ?? 'default'}`;
    const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
    const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
    const infostandBorderClass = borderId ? `border-${borderId}` : '';
    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;
        }
    };

    useOctaneEvent<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;
        });
    });

    useOctaneEvent<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;
            newValue.borderId = event.borderId ?? 0;
            return newValue;
        });
    });

    useOctaneEvent<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);
        setBorderId(avatarInfo.borderId ?? 0);

        SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));

        return () => {
            setRelationships(null);
        };
    }, [avatarInfo]);

    if (!avatarInfo) return null;

    const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
    const mottoMaxLength = GetConfigurationValue<number>('motto.max.length', 38);
    const showAchievementScore = GetConfigurationValue<boolean>('activity.point.display.enabled', true);
    const hasRelationships = !!relationships?.relationshipStatusMap.length;

    return (
        <NativeTextTree>
            <>
                <div className={`octane-infostand pointer-events-auto z-30 profile-card-background ${infostandCardBackgroundClass}`}>
                    {borderId ? <Base className={`infostand-border ${infostandBorderClass}`} /> : null}
                    <BaseButton type="button" className="octane-infostand__close" aria-label="Close" onClick={onClose} />
                    <div className="octane-infostand__header">
                        <BaseButton
                            type="button"
                            className="octane-infostand__home"
                            aria-label={LocalizeText('infostand.profile.link.tooltip')}
                            onClick={handleProfileClick}
                        >
                            <img src={homeIcon} alt="" draggable={false} />
                        </BaseButton>
                        <BaseButton type="button" className="octane-infostand__profile-link" onClick={handleProfileClick}>
                            <UserIdentityView
                                className="octane-infostand__identity"
                                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}
                            />
                        </BaseButton>
                        {isBlocked && <span className="text-[11px] italic opacity-80">{localizeWithFallback('infostand.blocked_user', 'Blocked user')}</span>}
                    </div>
                    <div className="octane-infostand__rule" />
                    <div className="octane-infostand__figure-row">
                        <BaseButton type="button" className={`octane-infostand__avatar-well 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}`} />
                        </BaseButton>
                        {avatarInfo.type === AvatarInfoUser.OWN_USER && (
                            <Base component={BaseButton} type="button"
                                className="background-edit-icon background-edit-position"
                                style={{ pointerEvents: 'auto', cursor: 'pointer' }}
                                onClick={handleEditClick}
                                aria-label="Edit profile background"
                            />
                        )}
                        <div className="octane-infostand__badges">
                            {(() => {
                                const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
                                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 component={BaseButton} type="button" disabled={avatarInfo.groupId <= 0}
                                            key="group"
                                            center
                                            className="relative h-[42px] w-[42px] bg-no-repeat bg-center"
                                            pointer={avatarInfo.groupId > 0}
                                            onClick={() => 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} />);
                                }
                                return items;
                            })()}
                        </div>
                    </div>
                    <div className="octane-infostand__rule" />
                    <div className="octane-infostand__motto">
                        {isOwnUser && <img src={pencilIcon} alt="" className="octane-infostand__pen" />}
                        {!isOwnUser && (
                            <Text fullWidth pointer textBreak wrap className="octane-infostand__motto-text" variant="white">
                                {motto}
                            </Text>
                        )}
                        {isOwnUser && !isEditingMotto && (
                            <Text component={BaseButton} type="button" align="start"
                                fullWidth
                                pointer
                                textBreak
                                wrap
                                className={`octane-infostand__motto-text ${motto ? '' : 'is-placeholder'}`}
                                variant="white"
                                onClick={() => setIsEditingMotto(true)}
                            >
                                {motto || LocalizeText('infostand.motto.change')} 
                            </Text>
                        )}
                        {isOwnUser && isEditingMotto && (
                            <>
                                <input
                                    autoFocus={true}
                                    className="motto-input"
                                    maxLength={mottoMaxLength}
                                    type="text"
                                    value={motto}
                                    onBlur={onMottoBlur}
                                    onChange={(event) => setMotto(event.target.value)}
                                    onKeyDown={onMottoKeyDown}
                                />
                                <span className="octane-infostand__motto-count">
                                    {motto.length}/{mottoMaxLength}
                                </span>
                            </>
                        )}
                    </div>
                    {showAchievementScore && (
                        <>
                            <div className="octane-infostand__rule" />
                            {isOwnUser ? (
                                <Hint content={localizeWithFallback('achievements.title', 'Achievements')}><BaseButton
                                    type="button"
                                    className="octane-infostand__score octane-infostand__score-link"
                                    onClick={() => CreateLinkEvent('achievements/show')}
                                >
                                    {LocalizeText('infostand.text.achievement_score')} {avatarInfo.achievementScore}
                                </BaseButton></Hint>
                            ) : (
                                <div className="octane-infostand__score">
                                    {LocalizeText('infostand.text.achievement_score')} {avatarInfo.achievementScore}
                                </div>
                            )}
                        </>
                    )}
                    {shownBadgesRank > 0 && (
                        <>
                            <div className="octane-infostand__rule" />
                            <BaseButton
                                type="button"
                                className="octane-infostand__score octane-infostand__score-link"
                                onClick={() => CreateLinkEvent('badge-leaderboard/show')}
                            >
                                {localizeWithFallback('infostand.text.badges_rank', `Badges rank: #${shownBadgesRank}`, ['rank'], [`#${shownBadgesRank}`])}
                            </BaseButton>
                        </>
                    )}
                    {avatarInfo.carryItem > 0 && (
                        <>
                            <div className="octane-infostand__rule" />
                            <Text small wrap variant="white">
                                {LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
                            </Text>
                        </>
                    )}
                    <div className={`octane-infostand__rule ${hasRelationships ? '' : 'octane-infostand__rule--footer'}`} />
                    <InfoStandWidgetUserRelationshipsView relationships={relationships} />
                    {GetConfigurationValue('user.tags.enabled') && (
                        <Column className="mt-1" gap={1}>
                            <InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
                        </Column>
                    )}
                </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}
                            selectedBorder={borderId}
                            setSelectedBorder={setBorderId}
                        />
                    </div>
                )}
            </>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
4En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser